Happens-Before原则

一、让人又爱又恨的指令重排

了解过Java并发编程知识的童鞋都知道,Java内存模型是围绕着并发过程中如何处理原子性、可见性和有序性3个特征来建立的,其中有序性最为复杂。
我们习惯性的认为代码总是从先到后、依次执行的,这在单线程的时候确实是没错的(至少程序是正确的运行的)。但在并发时,有时候给人感觉写在后面的代码,比写在前面的代码先执行,如同出现了幻觉。这就是鼎鼎大名的指令重排,指令重排是很有必要的,因为大大提高了cpu处理性能。
然而,指令重排,在提高了性能的同时,也会发生一些意想不到的灾难,举个栗子:

class UnsafeOrderExample {
  int x = 0;
  boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里 x 会是多少呢?
      System.out.print(x);
    }
  }
}

对于上面的代码,如果只有一个线程,先执行writer方法,然后再执行reader方法,会得到一个跟预期一致的结果是42。
但假设有两个线程A、B,两者同时分别执行writer和reader方法,最终reader会输出什么呢?很显然,答案是不固定的,有可能输出42,也有可能输出0;这是因为writer方法中的代码有可能发生指令重排,导致v=true有可能会发生在x=42之前。这个类是线程不安全类。
指令重排是必要的,但同时它又带来了一些麻烦,这可怎么办?别急,Java对此指定了Happens-Before规则,既然不能禁止指令重排,那就用规则对指令重排作约束,正所谓“爱,就是克制”嘛。

二、Happens-Before规则

正如前面所说,虽然jvm和执行系统会对指令进行一定的重排,但也是建立在一些原则上的,并非所有指令都可以随便改变执行位置。这些原则就是Happens-Before原则。Happens-Before可以直译为“先行发生”,但其想表达的更深层的意思是“前面操作的结果对后续操作是可见的”。所以比较正式的说法是:Happens-Before约束了编译器的优化行为,虽然允许编译器优化,但是编译器优化后一定要遵循Happens-Before原则。

1、程序顺序规则

程序顺序原则,指的是在一个线程内,按照程序代码的顺序,前面的代码运行的结果能被后面的代码可见。(准确的说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。)
举个简单栗子:

int a,b
a=1
b=a+1  //如果指令重排不遵循程序顺序原则,则b有可能等于1

如果指令重排不遵循程序顺序原则,以上的代码的b最终有可能等于1,而不是我们期望的2。这个原则就保证了程序语义的正确性,重排指令不允许改掉原来的代码语义。

2、传递性

传递性,指的是如果A Happens-Before于B,B Happens-Before于C,则A Happens-Before于C。这个是很好理解的。用白话说就是,如果A的操作结果对B可见,B操作结果对C可见,则A的操作结果对C也是可见的。

3、volatile变量规则

指对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作。如果单单是理解这句话的意思,就是我们熟悉的禁用cpu缓存的意思,使得volatile修饰的变量读到永远是最新的值。
如果这个规则跟第二个规则“传递性”结合来看,会有什么效果呢?我们可以通过改一下上面的例程来看看:

class UnsafeExample {
  int x = 0;
  volatile boolean v = false;//v用volatile修饰
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里 x 会是多少呢?
      System.out.print(x);
    }
  }
}

对比这段代码跟第一个例子中的代码,变化的只是成员变量v用了volatile修饰,如果仅仅是用“volatile变量规则”来看,如果同样是线程A、B同时分别调用writer和reader,得到的不也是有42或者0两个结果么?
别慌,如果我们再结合“传递性”规则来看:

  • x=42 Happens-Before 于写变量v=true
  • 写变量v=true Happens-Before 于读变量 v=true

根据“传递性”,可以得出x=42 Happens-Before 于读变量v=true,是不是恍然大悟了呢?由此可以得出最终B线程执行的reader方法输出的x=42而不是0。而这个结果,是靠“volatile变量规则”+“传递性”推导出来的,凭直觉是比较难看出来的。经过这样一番修改后,这个类就变成了线程安全了。

4、锁规则

锁规则,指的是一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
举个栗子:

synchronized (this) { // 此处自动加锁
  // x 是共享变量, 初始值 =10
  if (this.x < 12) {
    this.x = 12; 
  }  
} // 此处自动解锁

假设线程A执行完synchronized代码块后,x的值变成了12,线程B进入代码块时,可以看到线程A对x的修改,也就是能读到x==12。这个比较容易理解。

5、线程start()规则

指的是主线程A启动子线程B后,子线程B能看到主线程在启动线程B前的操作。
举个栗子:

Thread B = new Thread(()->{
  // 主线程调用 B.start() 之前
  // 所有对共享变量的修改,此处皆可见
  // 此例中,var==77
});
// 此处对共享变量 var 修改
var = 77;
// 主线程启动子线程
B.start();

此处,线程B能读到var==77。

6、线程join()规则

这个规则跟上一条规则有点类似,只不过这个规则是跟线程等待相关的。指的是主线程A等待子线程B完成(对B线程join()调用),当子线程B操作完成后,主线程A能看到B线程的操作。
举个栗子:

Thread B = new Thread(()->{
  // 此处对共享变量 var 修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66

此处,主线程A能看到线程B对共享变量var的操作,也就是可以看到var==66。

7、线程的interrupt()规则

指的是线程A调用线程B的interrupt()方法,Happens-Before 于线程B检测中断事件(也就是Thread.interrupted()方法)。这个也很容易理解。

8、finalize()规则

指的是对象的构造函数执行、结束 Happens-Before 于finalize()方法的开始。

三、总结

Happens-Before原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要一句,依靠这个原则,我们可以解决并发环境下两个操作之间是否存在冲突的所有问题。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值