Java的采用的是共享内存模型,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信。Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

jmm

原子性、可见性和有序性

JAVA内存模型主要是建立在如何处理java并发过程中的原子性、可见性和有序性这三个特征的:

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

  2. 可见性:可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。

  3. 有序性:Java语言提供了volatile和synchronized两个关键字来保证线程间操作的有序性。在java中,有序性可以总结为:如果在本线程内观察,所有操作都是有序的;如果在一个线程内观察另一个线程,所有操作都是无序的。前半句讲的是“线程内表现为串行语义”,后半句指“指令重排序”和“工作内存和主内存同步延迟”。

内存屏障

内存屏障:LoadLoad,StoreStore,LoadStore,StoreLoad

cpu

  1. LoadLoad

    Load1; LoadLoad; Load2: 确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。

  2. StoreStore

    Store1; StoreStore; Store2: 确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。

  3. LoadStore

    Load1; LoadStore; Store2: 确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。

  4. StoreLoad

    Store1; StoreLoad; Load2: 确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。StoreLoad是一个“全能型”的屏障,它同时具有其他三个屏障的效果。现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(buffer fully flush)。

重排序

重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序主要分为两类:编译器优化的重排序处理器重排序(指令级别并行的重排序和内存系统的重排序)。

对于编译器的重排序,JMM会根据重排序规则禁止特定类型的编译器重排序;对于处理器重排序,JMM会插入特定类型的内存屏障,通过内存的屏障指令禁止特定类型的处理器重排序。

volatile

volatile读的内存语义:
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

volatile写的内存语义:
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。

编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写与volatile写前面的任意内存操作重排序。为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

  1. 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
  2. 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

CAS操作同时具有volatile读和volatile写的内存语义。volatile变量的读/写和CAS可以实现线程之间的通信。

获取锁的内存语义:
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。

锁释放内存语义:
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

Lock锁释放-获取的内存语义的实现:
利用volatile变量的写-读所具有的内存语义。
利用CAS所附带的volatile读和volatile写的内存语义。

Java线程之间的通信的四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。

  2. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。

  3. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。

  4. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

final

对于final域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

  2. 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作的前面插入一个LoadLoad屏障。

as-if-serial语义

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序。

happen-before原则

  1. 程序次序规则:在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的操作happen-before(时间上)后执行的操作。

  2. 管理锁定规则:一个unlock操作happen-before后面(时间上的先后顺序,下同)对同一个锁的lock操作。

  3. volatile变量规则:对一个volatile变量的写操作happen-before后面对该变量的读操作。

  4. 线程启动规则:Thread对象的start()方法happen-before此线程的每一个动作。

  5. 线程终止规则:线程的所有操作都happen-before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

  6. 线程中断规则:对线程interrupt()方法的调用happen-before发生于被中断线程的代码检测到中断时事件的发生。

  7. 对象终结规则:一个对象的初始化完成(构造函数执行结束)happen-before它的finalize()方法的开始。

  8. 传递性:如果操作A happen-before操作B,操作B happen-before操作C,那么可以得出A happen-before操作C。

JMM对于程序员:如果一个操作happen-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作执行顺序排在第二个操作前面。

JMM对于重排序规则的约束原则:两个操作存在happen-before并不意味一定要按照happen-before的顺序执行,如果重排序的执行结果与按照happen-before的顺序执行结果一致,那么允许这种重排序。(JMM把happens-before要求禁止的重排序分为了下面两类:对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序;对于不会改变程序执行结果的重排序,JMM允许这种重排序。只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。)

JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证。

happen-before

JMM的内存可见性保证

Java程序的内存可见性保证按程序类型可以分为下列三类:

  1. 单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

  2. 正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是JMM关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。

  3. 未同步/未正确同步的多线程程序。JMM为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。

参考资料:

《Java并发编程的艺术》

深入理解java内存模型系列文章

Doug Lea并发编程文章全部译文

JAVA内存模型

CPU Cache与高性能编程

Cache一致性协议之MESI

内存屏障

缓存一致性

原子操作和竞争