Happens-Before 先行发生原则
Happens-Before 先行发生原则
参考资料: 《深入理解Java虚拟机》(周志明)
1. Happens-Before先行发生原则
是啥?
官方定义[https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5]
具象的描述一下什么叫先行发生原则Happens-Before
- 如果有两个操作
A
,B
A
在时间点T1
执行B
在时间点T2
执行, 且T2
在T1
之后- 若
A
的操作,能被B
操作观测到 - 则我们可以称
AB
两个操作满足了先行发生原则Happens-Before
2. Happens-Before先行发生原则
的用途
通过分析两个操作是否满足Happens-Before原则,
我们可以快速得知,两个操作的发生顺序,是否能满足我们的期望,
它是判断数据是否存在竞争,线程是否安全的手段
可以说是一种分析代码的方法论
我们先看两个例子,这两个例子是如何违反Happens-Before
的
2.1 违反先行发生原则(Happens-Before)的例子
- 为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化
- 计算之后,将乱序执行的结果重组,保证该结果与顺序执行结果是一致的
- 但不保证程序中各个语句计算的先后顺序与代码的顺序一致
- Java虚拟机的即时编译器中也有指令重排序(Instruction Recorder)优化
2.1.1 例1: 指令重排序导致的违反Happens-Before的例子
指令重排序[http://todo]
- 期望: 等到线程1执行完
doSomething()
之后,线程2再结束while循环
int a = 0; //成员变量
//线程1:
doSomething();
a = 1;
//线程2:
while(a==0){
doOtherThings();
}
- 上面这个例子,原本的意图是,当线程1将doSomething执行完,线程2结束
while循环
- 但是
doSomething()
和a=1
两个操作可能会被指令重排序, - 最终可能导致,线程1
doSomething()
还没有执行完,线程2就结束了while循环
2.1.2 例2: 多线程导致的问题的违反Happens-Before的例子
- 期望: 线程1
set(1)
之后,线程2即可print()
打印出新设置的值1
int a = 0; //成员变量
public void set(int a){
this.a = a;
}
public void print(){
System.out.println(this.a);
}
//线程1:
set(1);//T1 时间执行
//线程2:
print();//T2 时间执行,T2 在 T1之后
(预备知识虚拟机指令集[http://todo])
- 这个例子,按照时间顺序执行的时候会发生这种情况:
-
- 线程1调用
set()方法
,将线程1工作内存中的a
赋值为1;(工作内存assign赋值操作),此时主内存a=0
,工作内存a=1
- 线程1调用
-
- 然后,CPU让出时间片,交由线程2执行,线程2从主内存取值(主内存read读取操作),读出
a=0
,并打印出0
- 然后,CPU让出时间片,交由线程2执行,线程2从主内存取值(主内存read读取操作),读出
- 这种情况明显违反了我们的期望,我们期望的是在线程2中,读出
a=1
的结果 - 所以这违反了Happens-Before原则
2.2 怎么找到通用的方法去分析是否满足Happens-Before原则
经过2.1对两个实例进行了一波分析,我们判断出,这两个例子违背了Happens-Before原则
不能保证代码的顺序性和有效性
看起来这种分析不容易啊, 这种分析有没有固定套路呢?
有没有办法一下就能看出两个操作是否满足 Happens-Before原则?
往下看
3. 如何构造出满足Happens-Before原则
的代码?
- 在某些情况下我们必须要保证程序的顺序性,让程序符合我们的期望
- 那么我们就需要熟悉并利用JAVA内存模型中的规则来写代码
- 在JAVA内存模型下,有一些情况,保证了Happens-Before原则
如果两个操作之间的关系不在这些情况之中, 或者无法从这些规则推到出来, 则他们就没有顺序性保障,虚拟机可以对他们随意地进行重排序,这两个操作也就保证不了Happens-Before原则
下面我们进行一下列举
3.1 程序次序规则(Program Order Rule):
- 在一个线程内,按照控制流顺序,书写在前面的操作先于写在后面的操作
- 注意!控制流顺序不是代码书写的顺序,而是指判断分支,循环等结构!!!
//例3.1.1:
//正例,满足Happens-Before原则
int a = 10;
if(a == 10) doSomething();
//因为条件判断是需要依赖a的值,所以,这两行操作CPU不会对指令进行重排序
//例3.1.2:
//反例
int a = 10;
int b = 20;
//因为这两行操作并没有互相依赖,也没不涉及控制流,所以,CPU可能会对其进行指令重排序操作
3.2 管程锁定规则(Monitor Lock Rule):
- 一个
unlock
指令操作先行发生于后面对同一个锁的lock
指令操作 - 这里强调
同一个锁
,后面
是指时间上的先后
//例3.2.1:
//正例,满足Happens-Before原则
int i = 0;
public void a(){
synchronized(A.class){
i = 10;
//执行i=10的赋值操作,会执行 unlock 指令
}
}
public void b(){
synchronized(A.class){
//在这里会先执行 lock 指令
System.out.println(i);
}
}
//线程1: 在T1时刻调用
a();
//线程2: 在T2时刻调用, T2在T1后面
b();
//此例中,a()如果在前面(T1)调用
//那么,b()打印的值一定是10,一定是a()成功给i赋值后的值
//例3.2.2:
//反例
int i = 0;
public void a(){
i = 10;
}
public void b(){
System.out.println(i);
}
//线程1: 在T1时刻调用
a();
//线程2: 在T2时刻调用, T2在T1后面
b();
//此例中,b()有可能会打印出0,因为赋值操作不是一个原子操作,实际上被拆分成了多个字节码指令,一个字节码指令也可能对应多个机器指令,反正这就不是一个原子操作(埋个伏笔)
3.3 volatile变量规则 (Volatile Variable Rule):
- 对一个
volatile
变量的写操作 先行发生于后面对这个变量的读操作 - 这里后面同样是指时间上的先后
//例3.3.1:
//正例,满足Happens-Before原则
volatile int i = 0;
public void a(){
i = 10;
//在对有volatile修饰的变量赋值后会执行一个
//lock addl $0x0,(%esp)
//指令操作,把(ESP寄存器的值加0),是一个空操作
//他的作用是将本处理器的缓存写入内存(工作内存写入主内存),
//写入动作会引起别的处理器或者别的内核无效化(Invalidate)(涉及MESI等缓存一致性算法)
//通过这个字节码指令,可让volatile变量的修改对其他处理器立即可见
}
public void b(){
System.out.println(i);0
}
//线程1: 在T1时刻调用
a();
//线程2: 在T2时刻调用, T2在T1后面
b();
//此例中,a()如果在前面(T1)调用
//那么,b()打印的值一定是10,一定是a()成功给i赋值后的值
3.4 线程启动规则 (Thread Start Rule):
- Thread对象的
start()方法
先行发生于此线程的每一个动作
3.5 线程终止规则 (Thread Termination Rule):
- 线程中的所有操作都先行发生于对此线程的终止检测
- 我们可以用过
Thread::join()
方法时候结束,Thread::isAlive()
的返回值等手段检测线程是否已经终止
3.6 线程中断规则 (Thread Interruption Rule):
- 对线程
interrupt()方法
的调用先行发生于被中断线程的代码检测到中断事件的发生 - 可以通过
Thread::Interrupted()
方法检测到是否有中断发生
3.7 对象终结郭泽 (Finalizer Rule):
- 一个对象的初始化完成(构造函数执行结束)先行发生于它的
finalize()
方法的开始
3.8 传递性 (Transitivity):
- 如果
操作A
先行发生于操作B
操作B
先行发生于操作C
- 那么
操作A
先行发生于操作C
4. 工作中如何应用先行发生原则(Happens-Before)
工作中, 先行发生原则(Happens-Before) 最大的价值就是:
- 快速判断多线程的代码是否能保证期望的顺序性
- 如果两个操作在多线程情况下,需要有顺序保障
- 最简单的办法就是 保证他们满足 先行发生原则(Happens-Before)!
- 而如何满足先行发生原则? 看看第3节, 选一个自己构造即可
总结
至此我们应该搞明白以下几点:
先行发生原则(Happens-Before)
是什么- 如何判断两个操作是否满足
先行发生原则(Happens-Before)
- 如何写出满足
先行发生原则(Happens-Before)
代码
当然要能做到2,3两点还是需要对JVM有相当深刻的了解