學習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