参考文献

Java内存模型(JMM)

The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language. --Wikipedia

Java内存模型描述了Java编程语言中的线程如何通过内存进行交互.连同单线程代码执行的描述,内存模型提供了Java编程语言的语义.

《Java 虚拟机规范》中曾试图定义一种“Java 内存模型”(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果.

Java 内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节.

  • 在并发编程中,需要处理两个关键问题:线程之间如何通信以及线程之间如何同步
    • 通信是指线程之间以何种机制来交换信息.在命令是编程中,线程之间的通信机制有两种:共享内存和消息传递
      • 在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信.
      • 在消息传递的并发模型里,线程之间必须通过发消息来显示进行通信.
    • 同步是指程序中用于控制不同线程间操作发生相对顺序的机制.
      • 在共享内存的并发模型里,同步是显示进行的.程序员必须显示指定某个方法或某段代码需要在线程之间互斥执行.
      • 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的.
  • Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行的.整个通信过程对于程序员完全透明.
  • 所有变量(共享的)都存储在主内存中,每个线程都有自己的工作内存;工作内存中保存该线程使用到的变量的主内存副本拷贝.
  • 线程对变量的所有操作(读/写)都应该在工作内存中完成
  • 不同线程不能相互访问工作内存,交互数据要通过主内存

内存间交互操作

  • 关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节.
  • Java内存模型中定义了8种操作来完成.Java虚拟机实现时必须保证下面提及的每一种操作都是原子的,不可再分的.
操作 说明
lock(锁定) 作用于主内存的变量,它把一个变量的标识为一条线程独占的状态.
unlock(解锁) 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定.
read(读取) 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用.
load(载入) 作用于工作内存的变量,它把read操作从主内存中得到的变量放入工作内存的变量副本中.(相当于赋值的功能)
use(使用) 作用于工作内存的变量.它把工作内存中一个变量的值传递给执行引擎,
每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作.
assign(赋值) 作用于工作内存的变量,它把一个从执行引擎接收的值赋给工作内存的变量,
每当虚拟机遇到一个给变量赋值的字节码指令是执行这个操作.
store(存储) 作用于工作内存的变量,它把工作内存中一个变量的值传递到主内存中,以便随后的write操作使用.
write(写入) 作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中.
  • 如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read操作和load操作,如果要把变量从工作内存同步到主内存,就要按顺序执行storewrite操作.
    • 注意: Java内存模型只要求上述两个操作必须按顺序执行,但不要求连续执行.即readload,storewrite之间是可插入其他指令的.如对主内存中的变量a,b进行访问一种可能出现的顺序是read a,read b,load b,load a.
  • Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则:
    • 不允许readload,storewrite操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者工作内存发起回写了但主内存不接受的情况出现.
    • 不允许一个线程丢弃它最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存.
    • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中.即工作线程中读取的变量和主内存的变量的值是一样,从来没改变过,再同步到主内存中是没有意义的.
    • 一个新的变量只能在主内存中"诞生",不允许在工作内存中直接使用一个未被初始化(loadassign)的变量,换句话说就是对一个变量实施use,store操作之前,必须先执行assignload操作.
    • 一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁.
    • 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值在执行引擎使用这个变量前,需要重新执行loadassign操作以初始化变量的值.
    • 如果一个变量事先没有被lock操作,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量.
    • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store,write操作).

JMM与原子性,可见性,有序性

原子性

  • 由JMM直接保证的原子性变量操作包括read,load,assign,use,store,write六个.可以大致认为基本数据类型的访问,读写都是具有原子性的.
  • 若需要更大范围的原子性保证,JMM还提供了lockunlock操作来满足.尽管虚拟机未把lockunlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorentermonitorexit来隐式地使用这两个操作.这两个指令反映到Java代码就是同步块synchronized关键字.因此synchronized关键字具有原子性.

可见性

  • 在Java中提供volatile,synchronized,final三个关键字来实现可见性.

  • 对于volatile关键字,JMM是通过变量修改后将新值同步回主内存,在内存读取前从主内存刷新变量这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此.

    • 普通变量与volatile变量的区别是:volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新.
  • 对于synchronized关键字的可见性是由"对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store,write操作)."获得的.

  • 对于final关键字的可见性是指:被final修饰的字段在构造器中一旦被初始化完成,并且构造器没有把this的引用传递出去(this逸出),那么在其他线程中就能看到final字段的值.

有序性

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

JMM的抽象结构

  • 在Java中,所有实例域,静态域和数组元素都存储在堆内存中,堆内存在线程之间共享.局部变量(Local Variables),方法定义参数(Formal Method Parameters)和异常处理器参数(ExceptionHandler Parameters)不会在线程之间共享,它们不会存在可见性问题,也不受内存模型影响.
  • Java线程之间的通信有JMM控制,JMM决定了一个线程对变量的写入何时对另一个线程可见(可见性).
    • 从抽象的角度来看定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本.本地内存是JMM的抽象概念,并不真实存在.它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化.
img

重排序

  • Java 语言规范规定了JVM线程内部维持顺序化语义,也就是说只要程序的最终结果等同于它在严格的顺序化环境下的结果,那么指令的执行顺序就可能与代码的顺序不一致.这个过程通过叫做指令的重排序.

  • 指令重排序存在的意义在于:JVM能够根据处理器的特性(CPU的多级缓存系统、多核处理器等)适当的重新排序机器指令,使机器指令更符合CPU的执行特点,最大限度的发挥机器的性能.

重排序指的是:编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段.

指令重排序的前提是重排序指令不能影响结果

  • 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性.数据依赖分为下列3种类型:

    名称 代码示例 说明
    写后读 a=1;
    b=a;
    写一个变量之后,再读这个变量
    写后写 a=1;
    a=2;
    写一个变量,再写这个变量
    读后写 a=b;
    b=1
    读一个变量之后,再写这个变量
  • 上面情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变.而编译器和处理器可能会对操作做重排序,但是编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序.

    这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑.

重排序的种类

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

img

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果.
在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果.

as-if-serial语义

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

并发编程模型分类

  • 对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排(不是所有的编译器重排序都要禁止).对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel 称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序.

  • JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证.

  • JMM把内存屏障指令分为4类

    屏障类型 指令实例 说明
    LoadLoad Barriers Load1;LoadLoad;Load2 确保Load1数据的装载先于Load2及所有后续装载指令的装载
    StoreStore Barriers Srore1;StoreStore;Store2 确保Store1数据对其他处理器可见(刷新到内存)先于Store2及所有后续存储指令的存储
    LoadStore Barriers Load1;LoadStore;Store2 确保Load1数据的装载先于Store2及所有后续存储指令刷新到内存
    StoreLoad Barriers Store1;StoreLoad;Load2 确保Store1数据对其他处理器可见先于Load2及所有后续装载指令的装载.StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令.
    • StoreLoad Barriers同时具有其他3个屏障的效果.执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)

Happens-Before

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系.

happen-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性.

  • happens-before规则规定了对共享变量的写操作对其他线程可见,它是可见性与有序性的一套规则总结,抛开happens-before规则,JVM并不能保证一个线程对共享变量的写,可让其他线程对该共享变量的读可见;
  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前.
  • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行.如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法.
  • 真正要表达的是:前面一个操作的结果(结果包括修改了内存中共享变量的值,发送了消息,调用了方法等)对后续操作是可见的.

The new memory model semantics create a partial ordering on memory operations (read field, write field, lock, unlock) and other thread operations (start and join), where some actions are said to happen before other operations. When one action happens before another, the first is guaranteed to be ordered before and visible to the second. The rules of this ordering are as follows:

  • Each action in a thread happens before every action in that thread that comes later in the program’s order.

  • An unlock on a monitor happens before every subsequent lock on that same monitor.

  • A write to a volatile field happens before every subsequent read of that same volatile.

  • A call to start() on a thread happens before any actions in the started thread.

  • All actions in a thread happen before any other thread successfully returns from a join() on that thread.

    ​ --JSR-133

Happens-before原则

程序次序规则(Program Order Rule)

Each action in a thread happens before every action in that thread that comes later in the program’s order.

  • 在一个线程内,按照控制流顺序,书写在前面的操作先于发生于书写在后面的操作.这里说的是控制流顺序而不是程序代码顺序,因为要考虑分支,循环等结构.
  • 在单线程环境中,程序次序规则是必然保证的,即Java程序按照代码编写的顺序执行操作.
  • 在多线程环境中,程序次序规则遵循竞争锁的规则,即对于同一个锁,线程在执行过程中的所有操作都是按照程序代码的先后顺序进行的.因此,在多线程环境下,使用锁机制可以保证程序次序规则.
管程锁定规则(Monitor Lock Rule)

An unlock on a monitor happens before every subsequent lock on that same monitor.

  • 一个unlock操作先行发生于后面对同一个锁的lock操作.这里强调的是"同一个锁",而后面是指时间上的先后.
  • 如果多个线程同时访问同一个锁,那么就需要遵守Monitor Lock Rule,即每个线程在访问同一个锁之前必须先执行unlock操作,而每个线程在使用完锁后必须执行lock操作,否则就会出现并发访问共享数据的问题,导致数据出现错误或不一致。
volatile变量规则(Volatile Variable Rule)

A write to a volatile field happens before every subsequent read of that same volatile.

  • 对一个volatile变量写操作先行发生于后面对这个变量的读操作,这里"后面"同样是指时间上的先后.
  • 通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的.
线程启动规则(Thread Start Rule)

A call to start() on a thread happens before any actions in the started thread.

  • Thread对象的start()方法先行发生与此线程的每一个动作.
  • 假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见.
线程终止规则(Thread Termination Rule)
  • 线程中的所有操作都先行于对此线程的终止检测,可以通过Thread::join()方法是否结束,Thread::isAlive的返回值等手段检测线程是否已经终止执行.
  • 假定线程A在执行的过程中,通过指定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见.
线程中断规则(Thread Interruption Rule)
  • 对线程interrupt()方法的调用先行于发生于被中断线程的代码检测到中断时间的发生,
    • 可以通过Thread::interrupted()方法检测到是否有中断发生.
对象终结规则(Finalizer Rule)
  • 一个对象的初始化完成(构造函数执行结束)先发生于它的finalize()方法的开始.
跨线程规则(Thread-Ordering Rule)

If an action x happens-before another action y, then the execution of x in one thread happens-before the execution of y in another thread.

  • 如果一个操作x之间发生于另一个操作y,则线程中的操作x的执行先于发生于另一个线程中的操作y的执行.这个规则非常重要,是多线程编程正确性保证的基础之一.
传递性(Transitivity)
  • 如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A发生于操作C的结论.