Java記憶體模型(2)——happens-before
happens-before 原則 (先行發生原則)是JMM中最核心的概念,該原則闡述了操作之間的記憶體可見性。
happens-before的誕生——完善的JMM
Java語言是最早嘗試提供記憶體模型的語言,這是簡化多執行緒程式設計、保證程式可移植性的一個飛躍。早起類似C、C++等語言,並不存在記憶體模型的概念,其行為依賴於處理器本身的記憶體一致性模型,但不同的處理器差異很大,所以一段C++程式在處理器A上正常執行,並不能保證在B上也能得到同樣的結果。
Java對於記憶體模型的提出無疑具有劃時代的意義,但是問題的複雜度遠遠被低估了,隨著Java程式執行在越來越多的平臺之上,過於泛泛的記憶體模型定義會出現很多模稜兩可的地方,對於synchronized和volatile的指令重排序問題等也沒有做出明確規範。總結來說就是:
-
不能保證一些多執行緒程式的正確性(雙檢鎖失效問題)
-
不能保證同一段程式在不同處理器架構上表現一致,例如有的處理器支援快取一致性,有的不支援,各自都有自己的記憶體模型
基於以上的問題,Java迫切需要一個完善的JMM,能夠讓普通Java開發者和編譯器、JVM工程師達成清晰的共識,可以相對簡單並準確的判斷多執行緒程式什麼樣的執行序列是符合規範的。
-
對於編譯器、JVM工程師,關注點是如何使用類似記憶體屏障之類的技術,保證執行結果符合JMM的推斷
-
對於Java應用開發者,可能更加關注於volatile、synchronized等語義,如何利用類似 happens-before 規則,寫出可靠的多執行緒應用
下面是角色分工圖:
happens-before 是什麼?
happens-before(先行發生原則) 是Java記憶體模型用來保證多執行緒操作可見性的機制。先行發生是Java記憶體模型中定義的兩項操作之間的偏序關係:如果A操作先行發生於B操作,則操作A產生的影響將被B觀察到,其中影響包括:修改了記憶體中共享變數的值、傳送了訊息、呼叫了方法等。
我們知道,虛擬機器為了提高程式執行的效率,可能會對程式進行重排序,例如下面的示例:
//計算一個矩形的面積。公式:S = a * b
double a = 3; // A
double b = 5; // B
double S = a * b; // C
由於A和B操作之間沒有任何依賴關係,那麼A和B操作之間的順序是可以由虛擬機器進行重排序的,但是B和C之間卻禁止重排序,因為B和C之間存在資料依賴,如果進行重排序將會對結果產生影響。在單執行緒程式中,這種語義保證被稱為 as-if-serial語義,而在多執行緒執行程式中,就是 happens-before關係。
其實happens-before關係本質上就是as-if-serial語義:
-
as-if-serial語義保證單執行緒內程式的執行結果不會改變,happens-before關係保證正確同步的多執行緒程式的執行結果不被改變
-
as-if-serial語義讓編寫單執行緒程式的程式設計師有一種錯覺:單執行緒程式是按順序執行的。happens-before關係讓編寫多執行緒程式的程式設計師有一種錯覺:正確同步的多執行緒程式是按happens-before指定的順序來執行的
在上面示例程式碼中存在著3個happens-before關係:
-
A happens-before B (程式順序)
-
B happens-before C (程式順序)
-
A happens-before C (傳遞性)
JMM 把 happens-before 要求禁止的重排序分為如下兩類:
-
會改變程式執行結果的重排序(JMM要求編譯器和處理器必須禁止這種重排序)
-
不會改變程式執行結果的重排序(JMM對編譯器和處理器不做要求——JMM允許這種重排序)
happens-before 規則
下面介紹了JMM中一些天然存在的happens-before關係,這些happens-before關係無需任何同步協助器就已經存在,可以直接使用。
-
程式次序規則: 一個執行緒中的每個操作,先行發生於該執行緒的任意後續操作(從程式控制流順序考慮)
-
監視器鎖規則: 一個unlock操作先行發生於後面對同一個鎖的lock操作
-
volatile變數規則: 對於一個volatile變數的寫操作先行發生於後面對這個變數的讀操作
-
執行緒啟動規則: Thread物件的start()方法先行發生於此執行緒的每一個動作(例如:如果執行緒A執行了ThreadB.start(),那麼執行緒A的ThreadB.start()操作先行發生於B中的任意操作)
-
執行緒終止規則: 執行緒的所有操作都先行發生於對此執行緒的終止檢測(例如:如果執行緒A執行了ThreadB.join()併成功返回,那麼B中的所有操作都先行發生於A從ThreadB.join()的成功返回)
-
執行緒中斷規則: 對執行緒interrupt()方法的呼叫先行發生於被中斷執行緒的程式碼檢測到中斷事件的發生
-
物件終結規則: 一個物件的初始化完成(執行建構函式結束)先行發生於它的finalize()方法的開始
-
傳遞性: 如果操作A先行發生於操作B,操作B先行發生於操作C,那麼操作A先行發生於操作C
參考
《併發程式設計的藝術》
《深入理解Java虛擬機器》