Java内存模型

Published: by Creative Commons Licence

  • Tags:

Java内存模型

Java虚拟机试图定义一种Java内存模型(Jave memroy model, JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。此前,主流程序语言(C/C++)直接使用物理硬件和操作系统的内存模型,因此并发表现无法跨平台。

主内存与工作内存

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

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中,但是每条线程还拥有自己独立的工作内存(Working Memory)。线程中的工作内存保存了该线程使用到的存储在主内存中的变量的副本,线程堆变量的读写操作都必须在工作内存中进行。不同的线程之间无法访问彼此的工作内存。线程变量值的传递需要通过主内存来完成。

主内存与工作内存的划分,与Java数据分区(堆栈方法区等)并无直接联系。如果非要关联起来,主内存类似于栈堆,而工作内存类似于栈。

对于如果在工作内存和主内存之间进行同步,JMM定义了下面8种操作,虚拟机在实现时必须保证下面8种操作都是原子的:

命令 描述
lock 将主内存中的一个变量标识线程独占状态
unlock 解除主内存中的一个变量的线程独占状态
read 将一个变量的值从主内存向工作内存拷贝
load 将之前read操作得到的变量值放入工作内存的变量副本中
use 把工作内存中的一个变量的值传递给执行引擎
assign 从执行引擎接受一个值,并保存在工作内存
store 把工作内存中的变量的值传送到主内存中
write 把之前store传输来的变量放入主内存的变量中

JMM规定了下面虚拟机必须满足的规则

  • read和load必须成对出现,store和write也必须成对出现
  • assign后的值必须在之后通过store和write同步会主内存
  • store的值必须之前被assign过。
  • 只有主内存中可以创建新的变量,use、store一个变量之前,这个变量必须先执行过了assign和load操作。
  • 一个变量只允许一条线程进行lock操作,但同一条线程可以重复lock多次,但是unlock也必须执行相同次数才能解锁。
  • 如果一个变量执行lock操作,将会清空工作内存中此变量的值,之后如果要use该变量必须重新进行load和assign。
  • 线程不允许unlock未被其锁定的变量。
  • 对一个变量执行unlock之前,必须先把该变量同步会主内存中(store+write)。

volatile变量

volatile时java虚拟机提供的最轻量的同步机制。JMM为volatile专门定义了一些特殊的访问规则。

一个变量被描述为volatile后,它具有两种特性:

  1. 保证此变量的修改对所有线程可见(每次读写都从主内存进行同步)。
  2. 禁止指令重排序。

但是对volatile的写操作往往会被编译为多条字节码,即使被编译为一条字节码,其底层解释器可能也需要多条指令才能解释,因此volatile的写操作并不能保证具有原子性。

在某些情况下,volatile的同步机制的性能确实要优于锁。volatile的读操作的性能消耗与普通变量几乎没有什么区别,但是写操作会略慢一些。

原子性

我们可以认为基本数据类型的读写具备原子性。

如果需要更大范围的原子性保证,Java内存模型提供了lock和unlock操作,虚拟机未把lock和unlock直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式使用这两个操作。这两个字节指令对应于Java中的synchronized原语。

可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。JMM是通过在修改变量后将新值同步回主内存,在变量读取前从主内存刷新变量值这种利用主内存作为媒介的方式实现可见性的。

先行发生原则

先行发生原则是JMM中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,就是说A做出的修改能被B所观察到。

规则 描述
程序次序规则 在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作
监视器锁定规则 对同一个变量,一个unlock操作先行发生于后面对同一个锁的lock操作。
volatile变量规则 对一个volatile变量的写操作先行发生于后面对这个变量的读操作
线程启动规则 Thread对象的start方法先行发生于此线程的每一个动作
线程终止规则 线程中的所有操作都先行发生于对这个线程的终止检查
线程中断规则 对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生
对象析构规则 一个对象的构造器执行结束先行发生于它的finalize方法的开始。
传递性 如果A先行发生于B,B先行发生于C,则A先行发生于