并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

Posted 依码平川

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型相关的知识,希望对你有一定的参考价值。

      上一篇我们从硬件角度分析了缓存一致性问题产生的背景及解决方案,了解了处理器、高速缓存、缓存一致性协议与主内存之间的关系,然而,为什么我们在使用Java实际开发的过程中很少能够感知到这些处理呢?这就要从JAVA内存模型开始说起。

1. java内存模型的概念

       Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,JMM定义了共享内存系统中多线程程序读写操作行为的规范,以实现让Java程序在各个平台下都能达到一致的内存访问效果。

       Java内存模型的主要目标是定义程序中各个变量的访问规则,也就是在虚拟机中将变量存储到内存以及从内存中取出变量(这里的变量,指的是共享变量,也就是实例对象、静态字段、数组对象等存储在堆内存中的变量。而对于局部变量这类的,属于线程私有,不会被共享)这类的底层细节。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。

     它解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的可见性、原子性和有序性。内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。

2.主内存与工作内存

       Java内存模型定义了线程和内存的交互方式,在JMM抽象模型中,分为主内存、工作内存。主内存是所有线程共享的,工作内存是每个线程独有的。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存中的变量。并且不同的线程之间无法访问对方工作内存中的变量,线程间的变量值的传递都需要通过主内存来完成 :

并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

        如果大家还记得上一节我们提到的处理器-高速缓存-主存的交互关系图,对比后会发现两个图是极为相似,事实上,JAVA内存模型中定义的内存访问操作与硬件的缓存访问操作是具有可比性的:

- JMM的主内存相当于硬件的内存
- JMM的工作内存相当于处理器的寄存器和高速缓存

此外,了解JVM运行时数据区的同学可能会问:“这里的线程、工作内存与主内存与jvm运行数据区中的栈、堆、方法区等概念是否有关系?”       其实,二者并不是同一个层次的内存划分。如果两者一定要勉强对应起来,可以这么来理解:

- JMM的主内存对应于Java堆中对象的实例数据部分
- JMM的工作内存对应于Java虚拟机栈中的部分区域

       综上所述,可以得到JMM中的工作内存、主内存与硬件及java内存区域的对应关系可总结如下:

并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

3.内存间的8中交互操作

  • Lock(锁定):主内存变量锁定

  • Unlock(解锁):主内存变量解锁

  • Read(读取):主内存变量读取,将值传给工作内存,待Load

  • Load(载入):工作内存变量载入,将Read读到的值,存放到本地副本

  • Use(使用):把工作内存的变量传递给执行引擎

  • Assign(赋值):把从执行引擎接收到的的值赋值给工作内存变量

  • Store(存储):把工作内存的变量值传递给主内存,以便后续的write使用

  • Write(写入):用于主内存变量,把store获得的变量的值放入主内存变量

并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

4. 特殊规则

4.1 volatile型变量的特殊规则

 关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。

  • 被volatile修饰的变量有几大大特性

    1. 可见性

      并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

    2. 禁止指令重拍序

      底层原理是内存屏障,包括cpu的内存屏障和编译器的内存屏障 ,(java代码不好做演示,可翻阅jvm源码确认)

    3. 不保证原子性

      并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

      并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

  • volatile关键字的使用场景

     1. 运行结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值    

  • 2. 变量不需要与其他的状态变量共同参与不变约束

  • volatile 比其他同步工具快吗?

    • volatile变量读操作的性能消耗与普通变量几乎没有什么差别

    • 但是写操作上则可能会慢上一些,因为它需要在本地代码中插入需要内存屏障

    • 不过即便如此,大多数场景下volatile的总开销仍然要比锁来得低,我们在volatile与锁中选择的唯一判断依据进进是volatile的语义是否能满足业务场景的需求

    • 在某些情况下,volatile同步机制的性能要优于锁(synchronized关键字或juc包里面的锁)

    • 但是由于虚拟机对锁实行了许多消除和优化,使得我们很难量化地说volatile就会比synchronized快上多少

    • 如果volatile自己与自己比较,可以确定一个原则:

      volatile变量读操作的性能消耗与普通变量几乎没有什么差别但是写操作上则可能会慢上一些,因为它需要在本地代码中插入需要内存屏障,不过即便如此,大多数场景下volatile的总开销仍然要比锁来得低,我们在volatile与锁中选择的唯一判断依据进进是volatile的语义是否能满足业务场景的需求。

4.2 long和double型变量的特殊规则

  • long和double的非原子性协议

         Java内存模型要求lock,unlock,read,load,use,assign,store,write这八个操作都具有原子性,但是对于64位的数据类型(long和double)在JMM模型中定义了一条宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性 。

  • Java虚拟机对long和double变量的实际处理

           Java内存模型虽然允许虚拟机不把long和double变量的读写实现为原子操作,但允许虚拟机选择把这些操作实现为具有原子性的操作,而且还“强烈建议”虚拟机这样实现。

           在实际开发中,目前各种平台下的商用虚拟机几乎都选择把64位数据的读写操作作为原子操作来对待,因此我们再编写代码时,一般不需要将用到的long和double变量专门声明为volatile。

5. 原子性、可见性与有序性

5.1 原子性

JMM模型规定了基本数据类型都具有原子性(read,load,use,assign,store,write)

更大范围的原子性:JMM提供了lock,unlock操作来满足这种需求,虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorente和monitorexit来隐式地使用这两个操作,这两个字节码指令反应到Java代码块中就是同步块-synchronized关键字,因此在synchronized块之间的操作也具备原子性 。

并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型

      通过javap命令查看其对应生成的字节码,会发现synchronized底层是通过monitorenter和monitorexit指令实现锁的处理的。

  • 注:

    • 一般monitorenter和monitorexit是配对使用,这儿发现有一个monitorenter两个monitorexit,需要注意的是第二个monitorexit指令是为异常时的处理,异常时需要释放锁,否则会存在死锁风险。

5.2 可见性

  • 可见性就是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

  • Java中可以保证可见性的关键字:volatile,synchronized,final

    • volatile的可见性:特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。

    • synchronized的可见性: 是由“对一个变量执行unlock操作之前,必须先把变量同步回主内存中(执行store和write操作)”这条规则获得的。

    • final的可见性:被final修饰在字段再构造器中一旦被初始化完成,并且构造器没有把"this"的引用传递出去,那么其它线程中就能看见final字段的值

5.3 有序性

  • Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行语义”;后半句是指“指令重拍序”现象和工作内存与主内存同步延迟”现象。

  • Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。

    • volatile的有序性:volatile本身包含了禁止指令重拍序的语义。

    • synchronized的有序性:synchronize的有序性是由“一个变量同一时刻只允许一条线程对其进行lock操作”这条规则获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。

5.4 总结

  • synchronized: 同时满足原子性、可见性、有序性,貌似其是万能的,这也导致了部分开发人员对其滥用

  • volatile:满足可见性、有序性,不满足原子性,需要分析业务场景,确保正确使用

6.先行发生原则

6.1 什么是“先行发生”

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

6.2 先行发生的八大原则    

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

  • 程序次序规则(Program Order Rule)

    • 在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。

  • 监视器锁规则(Monitor Lock Rule)

    • 对一个监视器的unlock操作先行发生于后面对同一个锁的lock操作。

  • volatile变量规则 (Volatile Variable Rule)

    • 对一个volatile变量的写操作先行发生于后面对这个变量的读操作。

  • 线程启动规则 (Thread Start Rule )

    • Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程终止规则(Thread Termination Rule)

    • 线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行)

  • 线程中断规则 (Thread Interruption Rule )

    • 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。

  • 对象终结规则

    • 一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始

  • 传递性

    • 如果操作A发生于操作B,操作B先行发生于操作C,则可以得出操作A先行发生于C。

注:

时间上的先后顺序与先行发生原则:时间上的先后顺序与先行发生原则之间基本没有太大的关系,我们衡量并发安全问题的时候不要受到时间顺序的一切干扰,一切必须以现行发生原则为准。

7.总结

      本文详细介绍了JMM模型的相关概念及原则,此块内容非常重要,理论性很强,不太容易理解,需要反复琢磨,此块也是大厂面试的高频内容,希望对此块内容比较模糊的同学,重点研读下周志明的经典书籍《深入理解Java虚拟机》及方腾飞、魏鹏、程晓明合著的《Java并发编程的艺术》。

    线程的实现方式有哪些?java中的线程是如何实现的?java线程是如何调度的以及线程状态是如何转换的?关于这些内容,且听下回分解。


以上是关于并发编程3:深入理解Java虚拟机_内存模型与线程-JAVA内存模型的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM虚拟机读书笔记——内存模型与线程

深入理解Java的内存模型与线程并发问题

深入理解Java的内存模型与线程并发问题

深入理解Java的内存模型与线程并发问题

深入理解Java的内存模型与线程并发问题

《深入理解java虚拟机》---第12章 java内存模型与线程