指令重排
程序进行编译阶段都会对代码进行优化(即进行重排)来提高运行效率。指令重排会在不改变单线程程序予以的前提下,重新安排语句的指向顺序。
例如 singletion = new Singletion();
这句话的执行顺序为:
- 分配内存
- 初始化
- 对象指向内存空间
但在指令重排后可能为:
- 分配内存
- 对象指向内存空间
- 初始化
在并发情况下,指令重排会导致一个线程还没初始化或部分初始化就被另一个线程拿到,这是得到的值为空或错误。
所以在并发执行的情况下,指令重排会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
而解决这样的问题有两种方案:内存屏障(直接禁止指令重排)和 happens-before(指令重排必须按照一定的规则)。
happens-before
happens-before 并不是说前一个操作必须要在后一个操作之前执行,而是指前一个操作的执行结果必须对后一个操作可见,如果不满足这个要求,那就不允许这两个操作进行重排序。
happens-befoer 的几条规则:
- 程序次序规则(Program order rule):在一个线程内一段代码的执行结果是有序的。就是不管指令怎么重排,它宏观上的执行顺序跟我们代码的顺序生成是一致的。
- 线程锁规则(Monitor lock rule):就是不论在单线程还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁,它能够看到前一个线程的操作结果。
- volatile 变量规则(Volatile variable rule):如果一个线程先去写一个 volatile 变量,然后另一个线程去读这个变量时,之前的写操作的结果一定对读操作可见。
- 线程启动规则(Thread start rule):在主线程 A 执行过程中,启动子线程 B,那么线程 A 在启动子线程 B 之前对共享变量的修改结果对线程 B 可见。
- 线程终止规则(Thread termination rule):在主线程 A 执行过程中,子线程 B 终止,那么在线程 B 终止之前对共享变量的修改结果在线程 A 中可见。
- 线程中断规则(Interruption rule):主线程 A 调用线程 B 的 interrupt() 方法,必须在 B 检测到其被中断之前。(即 B 被中断前必须不能被中断)
- 传递规则(Transitivity):如果线程 A happends-before B,B happens-before C,那么 A happens-before C。(即 C 要可见 A 的结果)
- 对象终结规则:一个对象的初始化(构造方法执行)必须在对象 finalize() 之前。
规则实现原理
JVM 已经对这些规则做出了实现。接下来说说一些规则的实现原理。
volatile 实现原理
volatile 变量修饰的共享变量进行写操作的时候会使用 CPU 提供的 Lock 前缀指令:
- 将当前处理器缓存的数据写回到系统内存。
- 其他线程读取时会直接从系统内存中读取。
synchronized 的实现原理
使用 monitorenter 和 monitorexit 指令实现:
- monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处
- 每个 monitorenter 必须有对应的 monitorexit 与之配对。
- 任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。
final 的实现原理
- 编译器会在构造函数 return 之前插入一个 StoreStore 屏障。
- 读 final 域的重排序规则要求编译器在读 final 域的操作前插入一个 LoadLoad 屏障。