Java多线程之先行发生原则(happens-before)

本文首发于微信公众号:老胡码字java

前面介绍了Java内存模型及内存屏障相关概念,这篇文章接着介绍多线程编程另一个比较重要的概念:先行发生原则(happens-before)。编程

重要性

happens-before是判断数据是否存在竞争,线程是否安全的主要依据,经过这个原则,咱们能够解决并发环境下两个操做之间是否可能存在冲突的全部问题。安全

happens-before是什么

它Java内存模型中针对两项操做定义的偏序关系。例如操做A先行于操做B发生,那么操做B能够观察到操做A所产生的全部影响,这些影响包括修改内存中共享变量的值、发送的消息,调用的方法等。微信

举个例子:多线程

//该操做在线程1中执行
i = 1;

//该操做在线程2中执行
j = i;

//该操做在线程3中执行
i = 2
复制代码

上述例子中有共享变量i和j,假设线程1中执行的操做“i=1”先于线程2中的操做“j=i”发生,那共享变量j的值确定为1。并发

得出这个结论是由于根据happens-before原则:“i=1”操做能够被观察到,而线程3尚未开始。app

若是依旧假定操做“i=1”和操做“j=i”的先行发生关系,而线程3开始于线程1和线程2之间,线程3与线程2之间没有先行发生关系,最后变量j的值是多少呢?结果多是1也多是2。这种状况线程3对变量i的影响可能会被线程B观察到,也可能不会被观察到,当没有被线程B观察到的时候,线程B就会读取到过时的旧数据,这个时候就出现了多线程安全性问题。this

happens-before的8条规则

Java内存模型中已经存在8条定义好的先行发生规则,这些先行发生规则不须要任何同步操做就已经存在,若是两个操做之间的关系不在这几天规则或则没法经过这几条规则推导出来,那这两个操做就没有顺序保障,虚拟机就能够对他们进行重排。spa

具体操做以下:线程

  1. 程序次序规则:在同一个线程中,按照程序代码顺序,写在前面的操做先行发生与写在后面操做(控制流顺序:分支、循环等)。
  2. 锁规则:一个unlock操做先行发生于后面(时间上)对同一个锁的lock操做
  3. volatile变量规则:对一个volatile变量的写操做先行发生于后面对这个变量的读操做
  4. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个操做
  5. 线程终止规则:线程中的全部操做都先行发生于对此线程的终止检测(能够经过Thread.join()等待线程结束、Thread.isAlive()返回值)。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的检测到中断事件的代码
  7. 对象终结规则:一个对象的初始化完成先行发生于对象的finalize()方法的开始
  8. 传递性规则:若是操做A先行发生于操做B,操做B先行发生于操做C,那操做A就先行发生于操做C

happens-before规则示例

下面经过一个示例来看看如何经过这些规则来判断操做之间是否具备顺序性,而对于读写共享变量的操做来讲,就是现场是否安全。

private int value = 0;

public void setValue(int value) {
    this.value = value;
}

public int getValue() {
    return this.value;
}
复制代码

这个例子是一段很简单的代码,假定有线程1和线程2,线程1先(时间上先)调用"setValue(1)",而后线程2调用"getValue()",最后线程2获得的值是多少?

咱们根据前面提到的8个规则来分析下:

  • 由于两个方法分别在线程1和线程2中执行,不是同一个线程,所以程序次序规则不适用
  • 代码中没有任何同步代码块(锁),所以锁规则也不适用
  • 代码中value变量为非volatile变量,所以volatile变量规则也不适用
  • 整个执行过程很明显和线程启动、终止、中断、对象终结规则也没有关系
  • 由于没有任何先行发生关系,因此传递性规则也不适用

根据上面的分析,虽然线程1在操做时机上先于线程2,可是由于没有任何先行发生关系,因此没法肯定线程2中"getValue()"的值,所以这两个线程的操做放在一块儿是不安全的。

要解决这个问题也很简单,一种是将setValue和getValue两个方法都定义为synchronized方法,这样就能够套用锁规则,另一种是将value变量定义为volatile变量,并且这里修改value值的时候不依赖value的原值,因此就能够套用volatile变量规则。

经过上面的分析,咱们能够知道一个操做“时间上的先发生”不表明这个操做会“先行发生”。

那一个操做“先行发生”是否是“时间上也是先发生”呢,这个其实也是不能保证的,典型的例子就是指令重排,例以下面的例子:

//下面两个操做在同一个线程中执行
int i = 1;  //操做1
int j = 2;  //操做2
复制代码

上面的示例中,按照线程次序规则,操做1先行发生于操做2,可是操做2在实际执行过程当中极可能由于重排序而被处理器先执行,这样也没有影响先行发生原则的正确性,由于在这个线程中咱们没法感知到这种变化。

总结

时间上的前后顺序与先行发生原则之间没有基本的关系,所以咱们在衡量线程安全与否时不要关注时间顺序,而是应该关注先行发生原则。



下面是个人我的公众号,欢迎关注交流
相关文章
相关标签/搜索