一、JMM基础

1. 线程之间的通信机制

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。通信是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。在共享内存并发模里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型
,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

后续,会说明有哪几种通信方法。

2. JMM 抽象结构

image.png

在Java中,所有成员变量、静态变量和数组元素都存储在堆内存中,堆内存在线程之间共享,所以它们通常也称为共享变量。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory,或者也可以称为工作内存 Work Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

多个线程同时对同一个共享变量进行读写的时候会产生线程安全问题。那为什么CPU不直接操作内存,而要在CPU和内存间加上各种缓存和寄存器等缓冲区呢?因为CPU的运算速度要比内存的读写速度快得多,如果CPU直接操作内存的话势必会花费很长时间等待数据到来,所以缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾。

3. Java内存间的交互操作

image.png

image.png

  • read:把一个变量的值从主内存传输到工作内存中
  • load:在 read 之后执行,把 read 得到的值放入工作内存的变量副本中
  • use:把工作内存中一个变量的值传递给执行引擎
  • assign:把一个从执行引擎接收到的值赋给工作内存的变量
  • store:把工作内存的一个变量的值传送到主内存中
  • write:在 store 之后执行,把 store 得到的值放入主内存的变量中
  • lock:作用于主内存的变量,把一个变量标识为一条线程独占状态
  • unlock:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定

对应以上内存交互操作的相关规定:

  1. 不允许read和load、store和write操作之一单独出现,即不允许出现从主内存读取了而工作内存不接受,或者从工作内存回写了但主内存不接受的情况出现;
  2. 不允许一个线程丢弃它最近的assign操作,即变量在工作内存变化了必须把该变化同步回主内存;
  3. 不允许一个线程无原因地(即未发生过assign操作)把一个变量从工作内存同步回主内存;
  4. 一个新的变量必须在主内存中诞生,不允许工作内存中直接使用一个未被初始化(load或assign)过的变量,换句话说就是对一个变量的use和store操作之前必须执行过load和assign操作;
  5. 一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁。
  6. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值;
  7. 如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其它线程锁定的变量;
  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作;

注意:这里的lock和unlock是实现synchronized的基础,Java并没有把lock和unlock操作直接开放给用户使用,但是却提供了两个更高层次的指令来隐式地使用这两个操作,即monitorenter和monitorexit。

二、指令重排

在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。

1. 指令重排种类

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行

2. 图示

image.png

注意:如果两个操作访问同一个变量,其中一个为写操作,此时这两个操作之间存在数据依赖性。 编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序,即不会重排序。不管怎么重排序,单线程下的执行结果不能被改变,编译器、runtime和处理器都必须遵守as-if-serial语义。

名称说明:
as-if-serial 语义: 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变

三、内存屏障

屏障类型 示例 描述
LoadLoad Barriers Load1-LoadLoad-Load2 Load1数据装载过程要先于Load2及所有后续的数据装载过程
StoreStore Barriers Store1-StoreStore-Store2 Store1刷新数据到内存的过程要先于Strore2及后续所有刷新数据到内存的过程
LoadStore Barriers Load1-LoadStore-Store2 Load1数据装载要先于Strore2及后续所有刷新数据到内存的过程
StoreLoad Barriers Store1-StoreLoad-Load2 Store1刷新数据到内存的过程要先于Load2及所有后续的数据装载过程

注意:Java中volatile关键字的实现就是通过内存屏障来完成的。

四、Java内存模型的三大特性

1. 介绍

Java内存模型就是为了解决多线程环境下共享变量的一致性问题;一致性主要包含三大特性:原子性、可见性、有序性

2. 原子性:

  • 原子性是指一段操作一旦开始就会一直运行到底,中间不会被其它线程打断,这段操作可以是一个操作,也可以是多个操作。

    3. 可见性:

  • 可见性是指当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。

    4. 有序性

  • 如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。

  • 前半句是指线程内表现为串行的语义,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。

五、happens-before

1. 介绍

从JDK5开始,Java使用新的JSR-133内存模型,基于happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系,这个的两个操作既可以在同一个线程,也可以在不同的两个线程中。

2. happens-before规则:

  • 程序次序原则:
    • 在一个线程内,按照程序书写的顺序执行,书写在前面的操作先行发生于书写在后面的操作,准确地讲是控制流顺序而不是代码顺序,因为要考虑分支、循环等情况。
  • 监视器锁定原则:
    • 一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile域规则:
    • 对一个volatile变量的写操作先行发生于后面对该变量的读操作
  • 传递性规则:
    • 如果 A happens-before B,且 B happens-before C,那么A happens-before C。
  • 线程启动原则
    • 对线程的start()操作先行发生于线程内的任何操作。
  • 线程终止原则
    • 线程中的所有操作先行发生于检测到线程终止,可以通过Thread.join()、Thread.isAlive()的返回值检测线程是否已经终止。
  • 线程中断原则
    • 对线程的interrupt()的调用先行发生于线程的代码中检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否发生中断。
  • 对象终结原则
    • 一个对象的初始化完成(构造方法执行结束)先行发生于它的finalize()方法的开始。

注意:两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

3. 图示

image.png

解析:

  • 1 happens-before 2和3 happens-before 4由程序顺序规则产生。由于编译器和处理器都要遵守as-if-serial语义,也就是说,as-if-serial语义保证了程序顺序规则。因此,可以把程序顺序规则看成是对as-if-serial语义的“封装”。
  • 2 happens-before 3是由volatile规则产生。对一个volatile变量的读,总是能看到(任意线程)之前对这个volatile变量最后的写入。因此,volatile的这个特性可以保证实现volatile规则。
  • 1 happens-before 4是由传递性规则产生的。这里的传递性是由volatile的内存屏障插入策略和volatile的编译器重排序规则共同来保证的。

参考