深入理解JVM之先行发生关系(四)

在网上搜了一下「先行发生原则」,发现大部分都是将周志明的那本深入理解Java虚拟机里面的内容抄下来的,并没有解释先行发生原则(Happens-before)到底是怎么回事,为什么会有这个东西。在理解Happens-before,我觉得有必要先知道指令重排序是怎么回事。

指令重排序

为了使得机器能够更快的执行代码,完成指令运算,除了在硬件上有增加高速缓存等措施,还有一些方法就是对输入代码进行乱序执行优化,从而提高执行效率,而处理器会在计算之后将乱序的结果进行重组,保证结果与顺序执行的结果是一致的。但是并不保证各个语句计算的先后顺序与代码输入顺序一致。
与处理器乱序执行优化类似,Java虚拟机的即时编译器也有类似的指令重排序,指令重排序的出现归根到底是为了解决代码执行效率的问题,但是同时也带来了另外一个问题,就是如何保证重排序之后的指令执行结果与输入代码顺序的执行结果保持一致,比如单线程内部的需要顺序执行的代码,比如多线程对于同步代码块的执行等等。

Happens-before

Happens-before是JMM中定义两项操作的偏序关系,如果操作A和操作B满足Happens-before,比如操作A先行发生于操作B,那么操作B一定能看到操作A的影响。

如果仅靠synchronized和volatile来保证Java内存模型的有序性,那么我们日常代码的实现将无法想象。下面是JMM中天然的Happens-before关系,这些先行发生关系无需任何同步器协助就已经存在,可以在编码中直接使用。简单的说吧,如果两个操作的关系在此列满足下面的关系,就不会发生指令重排,该是咋样就是咋样。

程序次序规则、管程锁定规则、volatile变量、线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性

比如单线程肯定是顺序执行啊,不会出现乱七八糟的排序从而影响结果值;锁定的那个更好理解了,后来的另外的线程一定要等当前线程unlock之后,才能lock啊;还有volatile变量规则,也好理解啊,就是我先来的写操作,不能让后面的读操作排我前面去了啊,你后来的读操作一定要等我写完才执行读操作啊;其他的关于线程启动终止中断终结就更好理解了不说了,以上这些都是约定成俗的规定。你虽然有指令重排序这个大招可以提高程序执行效率,但是你见到了这些规则就不能乱排了,你得遵守我的这些规则,然后把这些规则起个名字,就成了Happens-before规则。

个人理解Happens-before规则的直接作用是约束指令重排序,从而保证同步,确定了线程的安全性。举例代码如下:

1
2
3
4
5
6
7
8
9
//线程A
private int value=0;
public void setValue(int value){
this.value=value;
}
//线程B
public int getValue(){
return value;
}

其中,假设线程A先调用了一个对象的setValue(1),然后线程B调用了同一个对象的getValue(),那么线程B收到的返回值是什么?
如果对照上述的先行发生规则,该案例操作不符合任意条规则,所以这里面的操作不是安全的。
那么怎么修复这个问题?有两个方案可以选择,要么是对getter、setter方法定义为synchronized方法,这样就可以套用官程锁定规则;要么把value定义为volatile变量,这样指令就不会重排,线程A先进行的setValue操作就一定先于后面线程B的getValue操作,符合volatile变量的规则。

-EOF-