JVM学习1: 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 执行, 且 T2T1 之后
  • 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 两个操作可能会被指令重排序,
  • 最终可能导致,线程1doSomething()还没有执行完,线程2就结束了while循环
2.1.2 例2: 多线程导致的问题的违反Happens-Before的例子
  • 期望: 线程1set(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. 线程1调用set()方法,将线程1工作内存中的a赋值为1;(工作内存assign赋值操作),此时主内存a=0,工作内存a=1
    1. 然后,CPU让出时间片,交由线程2执行,线程2从主内存取值(主内存read读取操作),读出a=0,并打印出0
  • 这种情况明显违反了我们的期望,我们期望的是在线程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节, 选一个自己构造即可

总结

至此我们应该搞明白以下几点:

  1. 先行发生原则(Happens-Before)是什么
  2. 如何判断两个操作是否满足先行发生原则(Happens-Before)
  3. 如何写出满足先行发生原则(Happens-Before)代码

当然要能做到2,3两点还是需要对JVM有相当深刻的了解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值