天天看点

JMM内存模型(总结)

Java内存模型(Java Memory Model,JMM)是Java虚拟机规范中定义的一种内存模型,用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下达到一致的内存访问效果。

主内存和工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量是指:实例字段、静态字段、和构成数组对象的元素,但不包括局部变量和方法参数,因为后者是线程私有的。

Java内存模型规定了所有的变量都存储在主内存中。每条线程拥有自己的工作内存,工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存来完成。

JMM内存模型(总结)

内存间交互操作

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主存拷贝至工作内存、如何从工作内存同步回主内存之类的实现细节。Java内存模型中定义了以下8种操作来完成,每一种操作都是原子的、不可再分的。

  • Lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来。
  • read(读取):作用于主内存变量,把2一个变量的值从主内存传输到线程的工作内存中,以便load动作使用。
  • load(载入):作用于工作内存的变量,把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量。把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存变量。把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
  • write(写入):作用于主内存变量,把store操作从工作内存中得到的变量的值放入主内存中。
注:如果要把一个变量从主内存复制到工作内存中,就要顺序执行read和load操作。Java内存模型只要求上述操作时顺序执行,没有保证是连续执行的,在read和load之间可以出现其他指令。

除此之外,Java内存模型还规定了在执行上述8中基本操作时必须满足的规则:

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

这8种内存访问操作以及上述规则限定,在加上稍后介绍的对volatile的一些特殊规定,就已经完全确定了Java程序中哪些内存访问操作在并发写是安全的。由于这种定义相当严谨但又十分繁琐,实践起来很麻烦。一般在实践中使用的是其等效判断原则——先行发生原则(happens-before),用来确定一个访问在并发环境下是否安全。

对于volatile变量的特殊规则

当一个变量定义为volatile之后,它将具备两种特性:

  • 可见性

当一条线程修改了这个变量的值,新值对于其他线程立即得知。

  • 禁止指令重排序

指令重排序:普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值的操作顺序与程序代码中一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within- Thread As-If-Serial Semantic)

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。

volatile总结

假设T表示一个线程,V和W分别表示两个volatile变量,那么有以下规则:

  • 只有当线程T对变量V执行的前一个动作是load时,线程T才能对变量执行use操作;并且,只有当线程T对变量V执行的最后一个动作是use时,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。
  • 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store操作;并且,只有当线程T对变量V执行的后一个动作是store时,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到线程可以看到自己对变量V所做的修改)。
  • 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似地,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。

原子性、可见性与有序性

Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这3个特征来建立的。

  • 原子性(Atomicity):由Java内存模型来直接保证原子性变量操作包括read、load、assign、user、store和write,我们大致认为基本数据类型的访问读写具有原子性(例外是long和double的非原子性协定)。需要更大范围的原子性保证可以使用synchronize关键字。
  • 可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。实现可见性的Java关键字有volatile、synchronize和final。
  • 有序性(Ordering):Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的(As-If-Serial);如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序和工作内存与主内存同步延迟现象)。实现有序性的Java关键字有volatile和synchronize。

先行发生原则(happens-before)

Java语言中有一个先行发生(happens-before)原则,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则解决并发环境下两个操作之间是否可能存在冲突的所有问题。

  • 程序次序规则(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()方法检测到是否有中断发生。
  • 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于finalize()方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C。

参考

《深入了解Java虚拟机》

继续阅读