学习happens-before的目的不是只限于知道这些规则的存在,而是要进一步知道如何实现和维护这些happens-before关系,在代码中加以注意。html
Happens-before 规则是从java代码设计层面保证有序性和可见性的机制。本文将会以图示、样例代码和解释相结合的方式,力图阐述清楚happens-before的原理,为理解如何保证线程安全性打下扎实的基础。java
关于happens-before关系,java语言说明中有以下的描述:编程
Two actions can be ordered by a happens-before relationship. If one action happens-before another, then the first is visible to and ordered before the second. -- Java Language Specification Ch 17.4.5. Happens-before Order
java代码编译成计算机指令后,计算机在执行指令时会进行必定程度的重排序。happens-before规则至关于java设计者和java开发者之间的约定,屏蔽了计算机底层的细节,从java层面和开发者创建约定,约定的内容是以下这样:安全
java语言的设计者创建了一套happens-before规则,若是开发者在相应的场景下确保两个操做之间听从了happens-before规则,那java会为开发者保证,听从了happens-before规则的多线程操做具备实际的happens-before关系,进而能够保证共享变量的可见性。多线程
这些happenns-before关系大多须要开发者本身保证,而于此同时,java内置就已经实现了一系列happens-before关系,这些关happens-before 关系自然存在,而不须要开发者自行维护。并发
网上绝大多数的文章在描述happens-before关系时,关于已经存在仍是须要开发者留心维护这个点上上没有说清楚。
官方文档所描述的happens-before并不是陈述句,不是在表述一个客观事实,而是一个应该补上一个should,完整的语义应该为: Action A should have a happens-before relationship with action B, in order to ensure result of action A is visible to action B.
若是咱们把须要咱们花气力维护的happens-before关系当成了自然存在的关系,例如,这样的一条规则:“* A write to a volatile field happens-before every subsequent read of that field.”,这是一条须要开发者维护的规则,若是开发者直接把这条规则当成了固有事实,不管怎样的代码顺序均可以保证“对volatile变量的写必定发生在读以前”,那么线程安全就彻底得不到保障了。
所以,后文对于happens-before介绍将主要分为两类:即已经存在
和开发者自行保证
。oracle
在学习下面的happens-before规则以前,咱们须要了解到的一个已有规则是,happens-before关系具备可传递性。用hb(a,b)表示a happens-before b,而若是 hb(b,c),则咱们能够得知hb(a,c)。可传递性这一固有特性将帮助咱们顺利理解后面的诸多规则。app
这一部分happens-before规则已经由java语言设计者实现,故对于开发者而言它们是固有存在的事实,了解他们有助于咱们清楚代码为何能够保证可见性,同时在复杂的状况有分析可见性问题的能力。ide
咱们,都知道,在单线程内部,对于开发者而言,能够确保写在前面的操做会happens-before以后的操做。尽管计算机底层的指令和java代码的顺序会有比较大的差别,但其中的细节jdk会去处理,java能够向开发者保证,单线程全部前面代码的操做结果对于后面代码是可见的。对于这一条规则无需赘述,绝大多数开发者在编码的第一天都是默认了这个事实。而这一规则也是其余全部happens-before规则成立的基石。学习
线程启动规则指出,线程A中启动线程B,那么线程A中在调用ThreadB.start()方法happens-before线程B 中的全部操做。因为可传递性的存在,咱们天然也能够得知,A线程中调用ThreadB.start()以前的全部操做均happens-before B线程的全部操做,即A线程在调用ThreadB.start()以前的操做结果对B线程可见。
该规则指出,线程B中的全部操做happens-before线程B的join()方法。同理,根据可传递性,咱们也能够肯定,线程B中的全部操做如statement1 happens-before 线程A的statement2。
这一部分的规则须要开发者额外关注,由于java语言没有内置机制保证这些happens-before规则,要实现这些场景下的可见性,须要开发者自行保证这一关系的存在。若是缺乏这些关系,则java没法保证一个线程的操做结果对另外一个线程的可见性。
若是但愿A线程的对volatile变量的修改对B线程可见,那么A线程对volatile的变量的写应该happens-before B线程对这个volatile变量的读。
即若是保证线程A对一个volatile变量的写发生在另外一个线程B对于这个变量的读以前,在A对这个变量的操做对B可见。
下面经过一个简单的代码样例验证这一规则。
public class TestVolatileHappensBefore { static volatile int shared = 0; static class WriteTask implements Runnable { @Override public void run() { shared = 5; //1. write to the volatile shared variable } } static class ReadTaslk implements Runnable { @Override public void run() { System.out.println(shared);//2. read to the volatile shared variable } } public static void main (String[] args) { Thread tWrite = new Thread(new WriteTask()); Thread tRead = new Thread(new ReadTaslk()); tWrite.start(); try { Thread.sleep(100); } catch (InterruptedException e) { //just wait to ensure order } tRead.start(); } }
这段代码的运行结果显而易见,会打印shared的值为5。由于听从了volatile变量的happens-before规则,故注释中1的操做结果对2可见。若是仅展现正例则没法证实这一规则存在的必要性。而下面的这个反例则能够证实咱们必须确保这一 happens-before 关系。
public static void main (String[] args) { Thread tWrite = new Thread(new WriteTask()); Thread tRead = new Thread(new ReadTaslk()); tRead.start(); tWrite.start(); }
反面例子中仅更改了对于volatile变量读写线程的开始顺序,即咱们没有严格遵照volatile变量的happens-before规则,那这样的代码运行结果会如何呢?这里附上数次运行结果的截图:
从结果能够看出,在短短数次的重复运行中,shared变量的值有时为初始值0,有时为被写进程修改后的值5。也就是更新后的shared变量的值没法保证其对其余线程的可见性。故若是须要保证volatile变量的可见性,咱们须要知足volatile变量的happens-before规则。
若是A线程是解锁操做,以后另外一个B线程执行加锁操做,若是但愿A的操做结果对B可见,那么 A应该happens-before B
一样经过一段样例代码展现该规则的正面案例,依然是线程先读后写的状况,同步机制依赖显示锁ReentrantLock:
public class TestLockHappensBefore { static int a = 0; static ReentrantLock lock = new ReentrantLock(); public static void modify1() { lock.lock(); try { System.out.println("a= " + a + " with " + Thread.currentThread().getName()); } finally { a = 10; lock.unlock(); } } public static void modify2() { lock.lock(); try { System.out.println("a= " + a + " with " + Thread.currentThread().getName()); } finally { a = 5; lock.unlock(); } } public static void main(String[] args) { Thread thread0 = new Thread() { @Override public void run() { modify1(); } }; Thread thread1 = new Thread() { @Override public void run() { modify2(); } }; thread0.start(); thread1.start(); } }
thread0打印当前a的值并将a赋值为10,thread1一样打印当前a的值并将其赋值为5。
由于thread0 符合happens-before thread1的规则,因此thread0对变量a的修改对thread1可见,故运行结果为如图:
而若是将启动顺序倒置
thread1.start(); thread0.start();
则关系变成了 thread1 happens-before thread0,则thread1对a的修改对thread0可见,运行结果如图:
在这个样例代码中不存在关系的违反,但知足不一样的happens-before关系,根据规则,则会有不一样的结果,这须要开发者在开发过程当中留心观察,理清逻辑。
这就是一个无穷无尽的话题了,在组合的场景下,只有符合了happens-before的线程两两之间可以保证可见性,而不必定能够保证全部线程的互相可见性,要处理好这种状况则须要开发者对
各类场景的happens-before规则烂熟于胸,利用好规则的可传递性,梳理清楚线程交互逻辑,才有可能处理好全部线程间的可见性问题。
一个小彩蛋
笔者在撰写样例代码的时候就遇到了这种组合的状况,happens-before关系并无造成传递链,故出现了意想不到的结果,仔细分析才得了缘由,与各位简单分享一下。代码以下:
public class LockUnexpectedHappensBefore { static int a = 0; public static synchronized void read() { System.out.println(a); } public static synchronized void write() { a = 5; } public static void main(String[] args) { Thread write = new Thread(){ public void run() { write(); } }; write.start(); read(); } }
看到这个代码,不知道各位认为打印出来的a的值会是多少,我一开始下意识认为必定是5,而后结果是初始值0。
之因此会有这样的结果,是由于尽管write 线程的start方法 happens-before 主线程调用read()方法,但由于建立线程和线程执行须要时间,write线程的run()方法并不被保证 happens-before 主线程的read()方法。
在这种状况下write线程的unlcok不被保证 happens-before 于 主线程的lock,主线程天然无法看到write线程的操做结果。而若是咱们想要read的结果打印a = 5,方法也很简单,运用咱们以前的几个固有的happens-before规则。
例如,应用 thread join规则,将代码改成:
write.start(); write.join(); read();
则write线程和主线程知足happens-before规则,具备实际的happens-before关系,在结合happens-before关系的传递性,happens-before关系从符合thread jion规则传递到符合锁的规则,故write线程的修改必定对主线程可见。仅仅是这样一个简单的样例尚且容易出错,在更复杂的开发场景下,要想处理好可见性问题,只能靠开发人员本身不断实践和总结,才能获得真知。
Java Language Specification
java并发核心编程78讲
Java - Understanding Happens-before relationship