熟悉Java并发编程的都知道,JMM (Java内存模型)中的之前(简称hb)规则,该规则定义了Java多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。
Java语言中有一个“先行发生”(之前)的规则,它是Java内存模型中定义的两项操作之间的偏序关系,如果操作一个先行发生于操作B,其意思就是说,在发生操作B之前,操作一个产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值,发送了消息,调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争,线程是否安全的主要依据。
<强>按照官方的说法:强>
当一个变量被多个线程读取并且至少被一个线程写入时,如果读操作和写操作没有HB关系,则会产生数据竞争问题。
要想保证操作B的线程看到操作一个的结果(无论A和B是否在一个线程),那么在A和B之间必须满足HB原则,如果没有,将有可能导致重排序。
当缺少HB关系时,就可能出现重排序问题。
这个大家都非常熟悉了应该,大部分书籍和文章都会介绍,这里稍微回顾一下:
-
<李>程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作,李>
<李>锁定规则:在监视器锁上的解锁操作必须在同一个监视器上的加锁操作之前执行。李>
<李>挥发性变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作,李>
<李>传递规则:如果操作一个先行发生于操作B,而操作B又先行发生于操作,则可以得出操作一个先行发生于操作C;李>
<李>线程启动规则:线程对象的开始()方法先行发生于此线程的每一个动作,李>
<李>线程中断规则:对线程中断()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,李>
<李>线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值手段检测到线程已经终止执行;李>
<李>对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始,李>
其中,传递规则我加粗了,这个规则至关重要。如何熟练的使用传递规则是实现同步的关键。
然后,再换个角度解释HB:当一个操作一个HB操作B,那么,操作一个对共享变量的操作结果对操作B都是可见的。
同时,如果操作B HB操作C,那么,操作一个对共享变量的操作结果对操作B都是可见的。
而实现可见性的原理则是缓存协议和记忆障碍。通过缓存一致性协议和内存屏障实现可见性。
在Doug Lea著作《Java并发性在实践中》中,有下面的描述:
书中提到:通过组合hb的一些规则,可以实现对某个未被锁保护变量的可见性。
但由于这个技术对语句的顺序很敏感,因此容易出错。
楼主接下来,将演示如何通过挥发性规则和程序次序规则实现对一个变量同步。
来一个熟悉的例子:
类ThreadPrintDemo { 静态int num=0; 静态不稳定的布尔标志=false; 公共静态void main (String [] args) { 线程t1=新线程(()→{ (;100比;num;){ 如果(!国旗,,(num==0 | | + + num % 2==0)) { System.out.println (num); 国旗=true; } } } ); 线程t2=新线程(()→{ (;100比;num;){ 如果(标志,,(+ + num % 2 !=0)) { System.out.println (num); 国旗=false; } } } ); t1.start (); t2.start (); } }
这段代码的作用是两个线程间隔打印出0 - 100的数字。
熟悉并发编程的同学肯定要说了,这个num变量没有使用不稳定,会有可见性问题,即:t1线程更新了num, t2线程无法感知。
哈哈,楼主刚开始也是这么认为的,但最近通过研究HB规则,我发现,去掉num的挥发性修饰也是可以的。
我们分析一下,楼主画了一个图:
我们分析这个图: