一、前言
Java Happens-Before是一组规则,来管理JVM和CPU如何通过指令重排来提升性能。通过调整多线程间变量相互依赖性,且不影响最终的结果,来达到提升并发性能。如果没有这个规则,那么指令的重排很可能导致最终的结果将是错的。
二、指令重排
现代CPU是多CPU或多核架构,我们在《小白六》提到过,因此,当指令相互没有依赖时,多CPU的架构可以并行执行;如果指令存在相互依赖,并行执行将可能出错。
- 指令没有相互依赖,可以并行执行
a = b + c
x = y + z
- 指令有相互依赖,不能并行执行
a = b + c
x = a + y
可以看到,第二条指令依赖第一条指令的计算结果,如果两条指令并行执行,x 的值将不正确。
- 指令部分依赖
a = b + c // 1
x = a + y // 2
i = j + k // 3
o = p + q // 4
上面这种情况,我们可以通过指令重排来并发执行,提升性能:
a = b + c // 1
i = j + k // 3
o = p + q // 4
x = a + y // 2
我们看到,指令重排只要不改变程序的语义(结果),那么是被允许的。
三、多CPU指令重排的问题
我们用生产一帧 / 绘制一帧 来举例:
public class ReOrderProblem {
private boolean hasNewFrame = false;
private Object frame = null;
private long taken = 0;
private long gen = 0;
public void genFrame(Object frame) {
this.frame = frame;
gen ++;
hasNewFrame = true;
}
public Object takeFrame() {
while (!hasNewFrame) {
// 等待...直到有新的frame
}
Object o = frame;
taken ++;
hasNewFrame = false;
return o;
}
}
在『genFrame』方法中,三条指令没有任何依赖,因为,JVM / CPU将可能通过指令重排来提升执行速度,如下:
public void genFrame(Object frame) {
gen ++;
hasNewFrame = true;
this.frame = frame;
}
在 JVM / CPU 角度看来,对象的引用传递可能相比其它两条指令要慢,因此,通过如上调整,可能性能会提升,但如果这么做了,就会造成『takeFrame』出问题(并不会引起 crash,但却可能绘制上一帧,因此浪费了一次 CPU)。
四、Happens-Before 规则
该规则的提出,就是用来解决,只要不改变程序的结果,那么指令重排是OK的,我们之前有稍微讲到过该规则,这里,我们将对其中几条具体分析。
4.1、(同一个线程中)程序顺序规则
在同一个线程中,按照程序的顺序,前面的优先于后面的操作执行。
我们也说过,再保证结果不变的情况下,可以进行指令重排,例如我们上面『指令没有相互依赖』或『指令部分依赖』时。而当有依赖时,则不允许指令重排。
a = b + c
x = a + y
此时,只能顺序执行,不允许指令重排。
4.2、监视器锁规则
我们讲过 synchronized ,因此知道,监视器锁是 Java 内置的锁,用来线程同步的。
synchronized(object) {
System.out.println("x = " + x);
x = 1;
}
两个线程,其中一个线程在同步块中,另一个因为锁而被阻塞;当在同步块中的线程退出时,另一个被阻塞的线程进入同步块时,x的值为上一个线程修改后的值。
4.3、volatile 写对后续的读可见
这个就是我们之前讲到过的,当一个 volatile 变量被修改时,JMM会让其它线程的工作内存该变量副本失效,后续操作需要从主内存中获取最新的值。
除了缓存失效,还有一点,我们需要『4.4 传递性』来一起看(涉及到volatile变量与普通变量的指令重排序问题)。
4.4、传递性
如果A操作先于B操作,B操作先于C操作,那么A操作先于C操作。这个可以理解。
好了,我们来看看『4.3』中关于 volatile 变量与普通变量的指令重排序:
class Example {
private int x = 10;
private volatile boolean v = false;
public void writer() {
x = 2;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
如下图分析

我们可以通过上图依次分析:
- 『x = 2』 Happens-Before 『v = true』(规则 1);
- 『v = true』Happens-Before 『读 v』(规则 3);
- 根据上面两条,再结果『规则 4』,我们可以得出结论:『读 x = 2』
这点是 JDK1.5 中 JSR-133 对 volatile 的增强!
4.5、线程 start 规则
这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
4.6、线程 join 规则
如果线程A执行线程B.join,并成功返回,那么,线程B的所有操作都优先于 join 返回操作。
五、总结
我们上一篇讲了 JMM,JMM分两部分,一部分就是 Happens-Before 规则,另一部分就是我们上一篇谈到的在JVM中的实现原理。只有掌握了JMM,我们才能更好的设计出健壮且正确的程序。
网友评论