Java并发读书笔记:JMM与重排序

Java内存模型(JMM)

Java内存模型(JMM)定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。编程

在Java中,全部实例域、静态域和数组元素都存在堆内存中,堆内存在线程之间共享,这些变量就是共享变量数组

局部变量(Local Variables),方法定义参数(Formal Method Parameters)和异常处理参数(Exception Handler Parameters)不会在线程之间共享,它们不存在内存可见性问题。缓存

JMM抽象结构

图参考自《Java并发编程的艺术》3-1
安全

上图是抽象结构,一个包含共享变量的主内存(Main Memory),出于提升效率,每一个线程的本地内存中都拥有共享变量的副本。Java内存模型(简称JMM)定义了线程和主内存之间的抽象关系,抽象意味着并不具体存在,还涵盖了其余具体的部分,如缓存、写缓存区、寄存器等。多线程

此时线程A、B之间是如何进行通讯的呢?并发

  • A把本地内存中的更新的共享变量刷新到主内存中。
  • B再从主内存中读取更新后的共享变量。

明确一点,JMM经过控制主内存与每一个线程的本地内存之间的交互,确保内存的可见性app

重排序

编译器和处理器为了优化程序性能会对指令序列进行从新排序,重排序可能会致使多线程出现内存可见性问题。性能

源码->最终指令序列

下图为《Java并发编程的艺术》3-3
学习

编译器重排序

  • 编译器优化的重排序:编译器不改变单线程程序语义的前提下,能够从新安排语句的执行顺序。

JMM对于编译器重排序规则会禁止特定类型的编译器重排序。

处理器重排序

  • 指令级并行的重排序:现代处理器采用指令级并行技术(Instruction-Level-Parallelism,ILP)将多条指令重叠执行。若是不存在数据依赖性,处理器能够改变语句对应及其指令的执行顺序。
  • 内存系统的重排序:处理器使用缓存和读/写缓冲区,使得加载和存储的操做看起来在乱序执行。

对于处理器重排序,JMM的处理器重排序会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,以禁止特定类型的处理器重排序。

数据依赖性

若是两个操做访问同一变量,且这两个操做中有一个为写操做,此时这两个操做之间就存在数据依赖性。

编译器和处理器会遵照数据依赖性,不会改变存在数据依赖关系的两个操做的执行顺序。(针对单个处理器中执行的指令序列和单个线程中执行的操做)

考虑抽象内存模型,现代处理器处理线程之间数据的传递的过程:将数据写入写缓冲区,以批处理的方式刷新写缓冲区,合并写缓冲区对同一内存地址的屡次写,减小内存总线的占用。但每一个写缓冲区只对它所在的处理器可见,处理器对内存的读/写操做可能就会改变。

as-if-serial

无论怎么重排序,(单线程)程序的执行结果不能被改变,一样,不会对具备数据依赖性的操做进行重排序,相应的,若是不存在数据依赖,就会重排序。

double pi = 3.14; // A 
double r = 1.0; // B 
double area = pi * r * r; // C
  • C与A访问同一变量pi、C与B访问同一变量r,且存在写操做,具备依赖关系,它们之间不会进行重排序。
  • A与B之间不存在依赖关系,编译器和处理器能够重排序,能够变成B->A->C。

很明显,as-if-serial语义很好地保护了上述单线程,让咱们觉得程序就是按照A->B->C的顺序执行的。

happens-before

从JDK5开始,Java使用新的JSR-133内存模型,使用happens-before的概念阐述操做之间的内存可见性。

有个简单的例子理解所谓的可见性和happens-before“先行发生”的规则。

i = 1;  //在线程A中执行
j = i;   //在线程B中执行

咱们对线程B中这个j的值进行分析:
假如A happens-before B,那么A操做中i=1的结果对B可见,此时j=1,是确切的。但若是他们之间不存在happens-before的关系,那么j的值是不必定为1的。

在JMM中,若是一个操做执行的结果须要对另外一个操做可见,两个操做能够在不一样的线程中执行,那么这两个操做之间必需要存在happens-before。

happens-before的规则

如下源自《深刻理解Java虚拟机》
意味着不遵循如下规则,编译器和处理器将会随意进行重排序。

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操做先行发生于书写在后面的操做。
  2. 监视器锁规则(Monitor Lock Rule):一个unLock操做在时间上先行发生于后面对同一个锁的lock操做。
  3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操做在时间上先行发生于后面对这个量的读操做
  4. 线程启动规则(Thread Start Rule):Thread对象的start()先行发生于此线程的每个动做。
  5. 线程终止规则(Thread Termination Rule):线程中的全部操做都先行发生于对此线程的终止检测。
  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成先行发生于它的finalize()方法的开始。
  8. 传递性(Transitivity):A在B以前发生,B在C以前发生,那么A在C以前发生。

happens-before关系的定义

  1. 若是A happens-before B,A的执行结果对B可见,且A的操做的执行顺序排在B以前,即时间上先发生不表明是happens-before。
  2. A happens-before B,A不必定在时间上先发生。若是二者重排序以后,结果和happens-before的执行结果一致,就ok。

举个例子:

private int value = 0;

public void setValue(int value){
    this.value = value;
}
public int getValue(){
    return value;
}

假设此时有两个线程,A线程首先调用setValue(5),而后B线程调用了同一个对象的getValue,考虑B返回的value值:

根据happens-before的多条规则一一排查:

  • 存在于多个线程,不知足程序次序的规则。
  • 没有方法使用锁,不知足监视器锁规则。
  • 变量没有用volatile关键字修饰,不知足volatile规则。
  • 后面很明显,都不知足。

综上所述,最然在时间线上A操做在B操做以前发生,可是它们不知足happens-before规则,是没法肯定线程B得到的结果是啥,所以,上面的操做不是线程安全的。

如何去修改呢?咱们要想办法,让两个操做知足happens-before规则。好比:

  • 利用监视器锁规则,用synchronized关键字给setValue()getValue()两个方法上一把锁。
  • 利用volatile变量规则,用volatile关键字给value修饰,这样写操做在读以前,就不会修改value值了。

重排序对多线程的影响

考虑重排序对多线程的影响:
若是存在两个线程,A先执行writer()方法,B再执行reader()方法。

class ReorderExample { 
    int a = 0; 
    boolean flag = false; 
    public void writer() { 
        a = 1;              // 1
        flag = true;        // 2 
    }
    Public void reader() { 
        if (flag) {         // 3 
            int i = a * a;  // 4
            …… 
        } 
    } 
}

在没有学习重排序相关内容前,我会坚决果断地以为,运行到操做4的时候,已经读取了修改以后的a=1,i也相应的为1。可是,因为重排序的存在,结果也许会出人意料。

操做1和2,操做3和4都不存在数据依赖,编译器和处理器能够对他们重排序,将会致使多线程的原先语义出现误差。

顺序一致性

数据竞争与顺序的一致性

上面示例就存在典型的数据竞争

  • 在一个线程中写一个变量。
  • 在另外一个线程中读这个变量。
  • 写和读没有进行同步。

咱们应该保证多线程程序的正确同步,保证程序没有数据竞争。

顺序一致性内存模型

  • 一个线程中的全部操做必须按照程序的顺序来执行。
  • 全部线程都只能看到一个单一的操做执行顺序。
  • 每一个操做都必须原子执行且马上对全部线程可见

这些机制实际上能够把全部线程的全部内存读写操做串行化

顺序一致性内存模型和JMM对于正确同步的程序,结果是相同的。但对未同步程序,在程序顺序执行顺序上会有不一样。

JMM处理同步程序

对于正确同步的程序(例如给方法加上synchronized关键字修饰),JMM在不改变程序执行结果的前提下,会在在临界区以内对代码进行重排序,未编译器和处理器的优化提供便利。

JMM处理非同步程序

对于未同步或未正确同步的多线程程序,JMM提供最小安全性。

1、什么是最小安全性?
JMM保证线程读取到的值要么是以前某个线程写入的值,要么是默认值(0,false,Null)。
2、如何实现最小安全性?
JMM在堆上分配对象时,首先会对内存空间进行清零,而后才在上面分配对象。所以,在已清零的内存空间分配对象时,域的默认初始化已经完成(0,false,Null)
3、JMM处理非同步程序的特性?

  1. 不保证单线程内的操做会按程序的顺序执行。
  2. 不保证全部线程看到一致的操做执行顺序。
  3. 不保证64位的long型和double型的变量的写操做具备原子性。(与处理器总线的工做机制密切相关)
  • 对于32位处理器,若是强行要求它对64位数据的写操做具备原子性,会有很大的开销。
  • 若是两个写操做被分配到不一样的总线事务中,此时64位写操做就不具备原子性。

总结

JMM遵循的基本原则:

对于单线程程序和正确同步的多线程程序,只要不改变程序的执行结果,编译器和处理器不管怎么优化都OK,优化提升效率,何乐而不为。

as-if-serial与happens-before的异同

异:as-if-serial 保证单线程内程序的结果不被改变,happens-before 保证正确同步的多线程程序的执行结果不被改变。
同:二者都是为了在不改变程序执行结果的前提下,尽量的提升程序执行的并行度


参考资料: 《Java并发编程的艺术》方腾飞 《深刻理解Java虚拟机》周志明

相关文章
相关标签/搜索