本文首发于微信公众号:老胡码字java
前面介绍了Java内存模型及内存屏障相关概念,这篇文章接着介绍多线程编程另一个比较重要的概念:先行发生原则(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
Java内存模型中已经存在8条定义好的先行发生规则,这些先行发生规则不须要任何同步操做就已经存在,若是两个操做之间的关系不在这几天规则或则没法经过这几条规则推导出来,那这两个操做就没有顺序保障,虚拟机就能够对他们进行重排。spa
具体操做以下:线程
下面经过一个示例来看看如何经过这些规则来判断操做之间是否具备顺序性,而对于读写共享变量的操做来讲,就是现场是否安全。
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,可是由于没有任何先行发生关系,因此没法肯定线程2中"getValue()"的值,所以这两个线程的操做放在一块儿是不安全的。
要解决这个问题也很简单,一种是将setValue和getValue两个方法都定义为synchronized方法,这样就能够套用锁规则,另一种是将value变量定义为volatile变量,并且这里修改value值的时候不依赖value的原值,因此就能够套用volatile变量规则。
经过上面的分析,咱们能够知道一个操做“时间上的先发生”不表明这个操做会“先行发生”。
那一个操做“先行发生”是否是“时间上也是先发生”呢,这个其实也是不能保证的,典型的例子就是指令重排,例以下面的例子:
//下面两个操做在同一个线程中执行
int i = 1; //操做1
int j = 2; //操做2
复制代码
上面的示例中,按照线程次序规则,操做1先行发生于操做2,可是操做2在实际执行过程当中极可能由于重排序而被处理器先执行,这样也没有影响先行发生原则的正确性,由于在这个线程中咱们没法感知到这种变化。
时间上的前后顺序与先行发生原则之间没有基本的关系,所以咱们在衡量线程安全与否时不要关注时间顺序,而是应该关注先行发生原则。