加入收藏 | 设为首页 | 会员中心 | 我要投稿 威海站长网 (https://www.0631zz.com/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 综合聚焦 > 资源网站 > 空间 > 正文

深入理解Java虚拟机(高效并发)

发布时间:2019-07-27 23:57:21 所属栏目:空间 来源:张磊BARON
导读:高效并发是 JVM 系列的最后一篇,本篇主要介绍虚拟机如何实现多线程、多线程间如何共享和竞争数据以及共享和竞争数据带来的问题及解决方案。 一. Java 内存模型与线程 让计算机同时执行多个任务,不只是因为处理器的性能更加强大了,更重要是因为计算机的

由 Java 内存模型来直接保证原子性变量操作,包括 read、load、assign、use、store 和 write ,我们大致可以认为基本数据类型的访问读写是具备原子性的。如果应用场景需要一个更大范围的原子性保证,Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管虚拟机未把 lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作,这两个字节码指令反映到 Java 代码中就是 synchronized 关键字,因此被 synchronize 修饰的方法或代码块之间的操作是具备原子性的。

可见性(Visibility)

可见性是指当一个线程修改了共享变量的值,其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是 volatile 变量都是如此,普通变量与 volatile 变量的区别是, volatile 的规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说 volatile 保证了多线程操作变量的可见性,而普通变量则不能保证这一点。除了 volatile 外,Java 还有两个关键字 synchronized 和 final 。synchronized 同步块的可见性是由「对一个变量执行 unlock 操作前,必须先把此变量同步回主内存中(执行 store、write 操作)」这条规则获得的;final 的可见性是指“:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有「this」的引用传递出去,那在其他线程中就能看见 final 字段的值。

有序性(Ordering)

Java 程序中天然的有序性可以总结为:如果在本线程内,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指「线程内表现为串行的语义」,后半句是指「指令重排序」现象和「工作内存和主内存同步延迟」现象。Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 关键字本身就包含了禁止指令重排的语义,而 synchronized 则是由「一个变量在同一时刻只允许一条线程对其进行 lock 操作」这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行的进入。

先行发生原则

如果 Java 内存模型中所有的有序性都仅仅靠 volatile 和 synchronized 来保证,那么有一些操作就会变得很繁琐,但是我们在编写 Java 并发代码的时候并没有感觉到这一点,这是因为 Java 语言中有一个「先行发生」(happens-before)原则。这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子解决并发环境下两个操作之间是否可能存在冲突的所有问题。

先行发生是 Java 内存模型中定义的两项操作之间的偏序关系,如果说操作 A 先行发生于操作 B,其实就是说在发生操作 B 之前,操作 A 产生的影响能被操作 B 观察到,「影响」包括修改了内存中共享变量的值、发送了消息、调用了方法等。

Java 内存模型下有一些天然的先行发生关系,这些先行发生关系无需任何同步器协助就已存在,可以在编码中直接使用。如果两个两个操作之间的关系不在此列,并且无法从下列规则推导出来,它们就没有顺序性保障,虚拟机就可以随意的对它们进行重排序。

  • 程序次序规则:在一个线程内,按照程序代码顺序,写在前面的代码先行发生写在后面的代码。准确的讲,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构;
  • 管程锁定规则:一个 unlock 操作先行发生于后面对于同一个锁的 lock 操作;
  • volatile 变量规则:对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作,理解了这个原则我们就能理解为什么 DCL 单例模式中为什么要用 volatile 来标识实例对象了;
  • 线程启动规则:线程的 start() 方法先行发生于此线程的所有其它动作;
  • 线程终止规则:线程中所有的操作都先行发生于对此线程的终止检测;
  • 程序中断规则:对线程 interrupt() 的调用先行发生于被中断线程的代码检测到中断时间的发生;
  • 对象终结规则:一个对象的初始化完成先行发生于它的 finalize() 的开始;
  • 传递性:操作 A 先行发生于 B,B 先行发生于 C,那么 A 就先行发生于 C。

3.Java 与线程

谈论 Java 中的并发,通常都是和多线程相关的。这一小节我们就讲讲 Java 线程在虚拟机中的实现。

线程的实现

主流的操作系统都提供了线程实现,Java 语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行 start() 且还未结束的 Thread 类的实例就代表了一个线程。Thread 类所有关键方法都是 Native 的。Java API 中,一个 Native 方法往往意味着这个方法没有使用或者无法使用平台无关的手段来实现(当然也可能是为了执行效率而使用 Native 方法,不过,通常最高效率的手段就是平台相关的手段)。

实现线程主要有 3 种方式:使用内核线程实现、使用用户线程实现、使用用户线程加轻量级进程混合实现。

Java 线程的实现

Java 线程在 JDK 1.2 之前是基于称为「绿色线程」的用户线程实现的。而在 JDK 1.2 中,线程模型替换为基于操作系统原生线程模型来实现。因此,在目前的 JDK 版本中,操作系统支持怎样的线程模型,在很大程度上决定了 Java 虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致,虚拟机规范中也没有限定 Java 线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对 Java 程序的编码和运行过程来说,这些差异都透明的。

4.Java 线程调度

线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式线程调度。

协同式线程调度

(编辑:威海站长网)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

推荐文章
    热点阅读