前面介绍了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还没有开始。
如果依旧假定操作“i=1”和操作“j=i”的先行发生关系,而线程3开始于线程1和线程2之间,线程3与线程2之间没有先行发生关系,最后变量j的值是多少呢?结果可能是1也可能是2。这种情况线程3对变量i的影响可能会被线程B观察到,也可能不会被观察到,当没有被线程B观察到的时候,线程B就会读取到过期的旧数据,这个时候就出现了多线程安全性问题。
Java内存模型中已经存在8条定义好的先行发生规则,这些先行发生规则不需要任何同步操作就已经存在,如果两个操作之间的关系不在这几天规则或则无法通过这几条规则推导出来,那这两个操作就没有顺序保障,虚拟机就可以对他们进行重排。
具体操作如下:
下面通过一个示例来看看如何通过这些规则来判断操作之间是否具有顺序性,而对于读写共享变量的操作来说,就是现场是否安全。
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在实际执行过程中很可能因为重排序而被处理器先执行,这样也没有影响先行发生原则的正确性,因为在这个线程中我们无法感知到这种变化。
时间上的先后顺序与先行发生原则之间没有基本的关系,因此我们在衡量线程安全与否时不要关注时间顺序,而是应该关注先行发生原则。
原创文章,严禁随意转载。
下面是我的个人公众号,欢迎关注交流