Java虚拟机之Java内存模型

本章很多内容参考了其它资料,但总体编排还是按照《深入理解Java虚拟机》的流程进行。

Java内存模型

Java虚拟机规范试图定义一种Java内存模型屏蔽掉各硬件和操作系统的内存访问差异,使Java在各平台下都能达到一致性的内存访问效果。

JSR-133中并没有Working MemoryMain Memory的概念,所以不纠结这些名词,规范中的定义为Shared variables/Heap memory,所有的实例字段,静态字段以及数组元素都存储其中。这里variables不包括局部变量和方法参数。较《深入》书中的关系图,下图可能更方便理解(规范中用了Heap memory一词,而方法区被规范描述为堆的逻辑部分p41):

1

Bridging The Gap Between The Java Memory Model And The Hardware Memory Architecture可以用下图表示:

2

也就是说,作图者认为,Thread Stack和Heap都可以被内存的各部分表示。所以内存模型对内存的划分,和实际物理内存以及Java内存区域的划分是没有关系的。详见:原文链接

内存间交互操作

原书有提JSR-133只用四种操作去定义Java内存模型的访问协议:

  • lock
  • unlock
  • read
  • write

具体内容不用赘述,为方便,以上被称为action。虽然描述方式改变了(以前八种现在四种),但是Java内存模型并没有改变。所以:

  • Thread Stack的变量改变必须同步回Heap;
  • 不允许一个线程无故把数据同步回Heap;
  • Thread Stack变量要初始化;
  • 执行多少次lock,就要执行多少次unlock操作才能解锁;
  • 变量被lock后,会清空Thread Stack变量的值,在使用变量之前,需要重新初始化;
  • unlock前必须有lock;
  • unlock后需要同步回Heap;

再加上volatile的特殊规定,就完全确定Java程序中哪些内存访问操作在并发下是安全的。

volatile

两种特性:

  • 保证此变量对所有线程可见;
  • 禁止指令重排;

原书有这么一句话:“这里的‘可见性’是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知。”

这里的“立即得知”,我的理解是,立即得知改变了,而不是得知新值。其它线程获取新值仍需要对主存进行read。下面简单了解一下volatile实现原理:

在线程对volatile修饰的变量进行write的时候,比起修改不同代码,会在程序运行时多产生一行汇编代码,这行代码包含一个lock前缀的指令,这个指令会引发两件事:

  • 将当前处理器的缓存行的数据写回共享内存;
  • 这个回写操作会使其它CPU缓存了该内存地址的数据失效;

第二条的“失效”,对应“立即得知”。至于其它线程何时获得新值,《Java并发编程的艺术》写到:“下次访问相同内存地址时。”不过本人认为,无论是下次访问还是立即修改,都没有破坏Java内存模型。

volatile变量的运算在并发下是不安全的,从前文也可以看出,volatile的“锁”作用于汇编代码中的指令,如果说它的操作是“原子操作”,那么它的“锁”操作就是“夸克操作”。类似一个变量的++,对应的就是多条字节码指令,而一条字节码指令往往又可能转化为若干机器码指令,多线程执行会产生并发问题。

所以当破坏了下面两个原则,volatile还是要加锁:

  • 运算结果不依赖变量的当前值,或者能确保只有单一的线程修改变量的值;
  • 变量不需要与其它的状态变量共同参与不变约束;

指令重排的目的是为了优化性能(Out-Of-Order Execution优化)。单线程下编译器和处理器的重排会遵守数据依赖性,但是编译器和处理器并不会考虑多处理器间和多线程间的数据依赖。

名称 代码示例 说明
写后读 a = 1; b = a; 写一个变量后,再读这个位置
写后写 a = 1; a = 2; 写一个变量后,再写这个变量
读后写 a = b; b = 1; 读一个变量后,再写这个变量

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果就会被改变。

那volatile是如何做到禁止指令重排的呢?它会生成一条lock前缀指令,起到了内存屏障的作用。这条指令会把修改同步到内存,那么这就意味着之前的操作都已经完成,好似一道“屏障”。

所以volatile变量操作和普通变量操作在性能上最大的区别,是在写操作上,因为会在程序执行的过程中插入很多内存屏障指令。

前面提到了Java内存模型对volatile变量的定义有特殊规定,这些规定包括:

  • Thread Stack变量在使用前必须从内存刷新最新的值,用于保证能看到其它线程对该变量的修改;
  • 对Thread Stack变量修改后要同步回Heap,用于保证其它线程能看到本线程对该变量的修改;
  • 对volatile修饰的变量不能被指令重排,保证代码的执行顺序和程序的顺序相同;

原子性、可见性和有序性

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

这三点无需多谈,要注意的是,volatile之外,synchronized和final都可以实现可见性,synchronized可以满足三种特性,但滥用会带来性能影响。

happens-before原则

  • 程序次序原则:在一个线程里,按程序代码顺序,书写在前面的操作先行发生于书写在后面的操作(单线程下处理器和编译器会遵守依赖性)。
  • 程序锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作,“后面”指时间上的顺序;
  • volatile变量原则:对volatile变量操作先发生于后面对这个变量的读操作,“后面”指时间上的顺序;
  • 线程启动规则:start()先发生于每个操作;
  • 线程终止规则:线程中所有操作都先行发生于对这个线程的终止检测;
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中中断事件的发生;
  • 对象终结规则:一个对象的初始化完成先行发生于它的finalize();
  • 传递性:A先发生于B,B先发生于C,A就先发生于C;

这个原则,字面意思解释很清楚。

小结

本章主要是罗列出一些在学习中有疑惑并需要了解的点,学习的过程也引用了其它的相关资料: