JMM核心概念之Happens-before原則
一、前言
關(guān)于 Happens-before,《Java 并發(fā)編程的藝術(shù)》書中是這樣介紹的:
Happens-before 是 JMM 最核心的概念。對應(yīng) Java 程序員來說,理解 Happens-before 是理解 JMM 的關(guān)鍵。
《深入理解 Java 虛擬機 - 第 3 版》書中是這樣介紹的:
Happens-before 是 JMM 的靈魂,它是判斷數(shù)據(jù)是否存在競爭,線程是否安全的非常有用的手段。
我想,這兩句話就已經(jīng)足夠表明 Happens-before 原則的重要性。
那為什么 Happens-before 被不約而同的稱為 JMM 的核心和靈魂呢?
二、JMM 設(shè)計者的難題與完美的解決方案
事實上,從 JMM 設(shè)計者的角度來看,可見性和有序性其實是互相矛盾的兩點:
- 一方面,對于程序員來說,我們希望內(nèi)存模型易于理解、易于編程,為此 JMM 的設(shè)計者要為程序員提供足夠強的內(nèi)存可見性保證,專業(yè)術(shù)語稱之為 “強內(nèi)存模型”。
- 而另一方面,編譯器和處理器則希望內(nèi)存模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優(yōu)化(比如重排序)來提高性能,因此 JMM 的設(shè)計者對編譯器和處理器的限制要盡可能地放松,專業(yè)術(shù)語稱之為 “弱內(nèi)存模型”。
對于這個問題,從 JDK 5 開始,也就是在 JSR-133 內(nèi)存模型中,終于給出了一套完美的解決方案,那就是 Happens-before 原則,Happens-before 直譯為 “先行發(fā)生”,《JSR-133:Java Memory Model and Thread Specification》對 Happens-before 關(guān)系的定義如下:
1)如果一個操作 Happens-before 另一個操作,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。
2)兩個操作之間存在 Happens-before 關(guān)系,并不意味著 Java 平臺的具體實現(xiàn)必須要按照 Happens-before 關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按 Happens-before 關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法(也就是說,JMM 允許這種重排序)
并不難理解,第 1 條定義是 JMM 對程序員強內(nèi)存模型的承諾。從程序員的角度來說,可以這樣理解 Happens-before 關(guān)系:如果 A Happens-before B,那么 JMM 將向程序員保證 — A 操作的結(jié)果將對 B 可見,且 A 的執(zhí)行順序排在 B 之前。注意,這只是 Java內(nèi)存模型向程序員做出的保證!
需要注意的是,不同于 as-if-serial 語義只能作用在單線程,這里提到的兩個操作 A 和 B 既可以是在一個線程之內(nèi),也可以是在不同線程之間。也就是說,Happens-before 提供跨線程的內(nèi)存可見性保證。
針對這個第 1 條定義,我來舉個例子:
// 以下操作在線程 A 中執(zhí)行 i = 1; // a // 以下操作在線程 B 中執(zhí)行 j = i; // b // 以下操作在線程 C 中執(zhí)行 i = 2; // c
假設(shè)線程 A 中的操作 a Happens-before 線程 B 的操作 b,那我們就可以確定操作 b 執(zhí)行后,變量 j 的值一定是等于 1。
得出這個結(jié)論的依據(jù)有兩個:一是根據(jù) Happens-before 原則,a 操作的結(jié)果對 b 可見,即 “i=1” 的結(jié)果可以被觀察到;二是線程 C 還沒運行,線程 A 操作結(jié)束之后沒有其他線程會修改變量 i 的值。
現(xiàn)在再來考慮線程 C,我們依然保持 a Happens-before b ,而 c 出現(xiàn)在 a 和 b 的操作之間,但是 c 與 b 沒有 Happens-before 關(guān)系,也就是說 b 并不一定能看到 c 的操作結(jié)果。那么 b 操作的結(jié)果也就是 j 的值就不確定了,可能是 1 也可能是 2,那這段代碼就是線程不安全的。
再來看 Happens-before 的第 2 條定義,這是 JMM 對編譯器和處理器弱內(nèi)存模型的保證,在給予充分的可操作空間下,對編譯器和處理器的重排序進行一定的約束。也就是說,JMM 其實是在遵循一個基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。
JMM 這么做的原因是:程序員對于這兩個操作是否真的被重排序并不關(guān)心,程序員關(guān)心的是執(zhí)行結(jié)果不能被改變。
文字可能不是很好理解,我們舉個例子,來解釋下第 2 條定義:雖然兩個操作之間存在 Happens-before 關(guān)系,但不意味著 Java 平臺的具體實現(xiàn)必須要按照 Happens-before 關(guān)系指定的順序來執(zhí)行。
int a = 1; // A int b = 2; // B int c = a + b; // C
根據(jù) Happens-before 規(guī)則(下文會講),上述代碼存在 3 個 Happens-before 關(guān)系:
1)A Happens-before B
2)B Happens-before C
3)A Happens-before C
可以看出來,在 3 個 Happens-before 關(guān)系中,第 2 個和第 3 個是必需的,但第 1 個是不必要的。
也就是說,雖然 A Happens-before B,但是 A 和 B 之間的重排序完全不會改變程序的執(zhí)行結(jié)果,所以 JMM 是允許編譯器和處理器執(zhí)行這種重排序的。
看下面這張 JMM 的設(shè)計圖更直觀:

其實,可以這么簡單的理解,為了避免 Java 程序員為了理解 JMM 提供的內(nèi)存可見性保證而去學(xué)習(xí)復(fù)雜的重排序規(guī)則以及這些規(guī)則的具體實現(xiàn)方法,JMM 就出了這么一個簡單易懂的 Happens-before 原則,一個 Happens-before 規(guī)則就對應(yīng)于一個或多個編譯器和處理器的重排序規(guī)則,這樣,我們只需要弄明白 Happens-before 就行了。

三、8 條 Happens-before 規(guī)則
《JSR-133:Java Memory Model and Thread Specification》定義了如下 Happens-before 規(guī)則, 這些就是 JMM 中“天然的” Happens-before 關(guān)系,這些 Happens-before 關(guān)系無須任何同步器協(xié)助就已經(jīng)存在,可以在編碼中直接使用。如果兩個操作之間的關(guān)系不在此列,并且無法從下列規(guī)則推導(dǎo)出來,則它們就沒有順序性保障,JVM 可以對它們隨意地進行重排序:
1)程序次序規(guī)則(Program Order Rule):在一個線程內(nèi),按照控制流順序,書寫在前面的操作先行發(fā)生(Happens-before)于書寫在后面的操作。注意,這里說的是控制流順序而不是程序代碼順序,因為要考慮分支、循環(huán)等結(jié)構(gòu)。
這個很好理解,符合我們的邏輯思維。比如我們上面舉的例子:
int a = 1; // A int b = 2; // B int c = a + b; // C
根據(jù)程序次序規(guī)則,上述代碼存在 3 個 Happens-before 關(guān)系:
- A Happens-before B
- B Happens-beforeC
- A Happens-before C
2)管程鎖定規(guī)則(Monitor Lock Rule):一個 unlock 操作先行發(fā)生于后面對同一個鎖的 lock 操作。這里必須強調(diào)的是 “同一個鎖”,而 “后面” 是指時間上的先后。
這個規(guī)則其實就是針對 synchronized 的。JVM 并沒有把 lock 和 unlock 操作直接開放給用戶使用,但是卻提供了更高層次的字節(jié)碼指令 monitorenter 和 monitorexit 來隱式地使用這兩個操作。這兩個字節(jié)碼指令反映到 Java 代碼中就是同步塊 — synchronized。
舉個例子:
synchronized (this) { // 此處自動加鎖
if (x < 1) {
x = 1;
}
} // 此處自動解鎖
根據(jù)管程鎖定規(guī)則,假設(shè) x 的初始值是 10,線程 A 執(zhí)行完代碼塊后 x 的值會變成 1,執(zhí)行完自動釋放鎖,線程 B 進入代碼塊時,能夠看到線程 A 對 x 的寫操作,也就是線程 B 能夠看到 x == 1。
3)volatile 變量規(guī)則(Volatile Variable Rule):對一個 volatile 變量的寫操作先行發(fā)生于后面對這個變量的讀操作,這里的 “后面” 同樣是指時間上的先后。
這個規(guī)則就是 JDK 1.5 版本對 volatile 語義的增強,其意義之重大,靠著這個規(guī)則搞定可見性易如反掌。
舉個例子:

假設(shè)線程 A 執(zhí)行 writer() 方法之后,線程 B 執(zhí)行 reader() 方法。
根據(jù)根據(jù)程序次序規(guī)則:1 Happens-before 2;3 Happens-before 4。
根據(jù) volatile 變量規(guī)則:2 Happens-before 3。
根據(jù)傳遞性規(guī)則:1 Happens-before 3;1 Happens-before 4。
也就是說,如果線程 B 讀到了 “flag==true” 或者 “int i = a” 那么線程 A 設(shè)置的“a=42”對線程 B 是可見的。
看下圖:

4)線程啟動規(guī)則(Thread Start Rule):Thread 對象的 start() 方法先行發(fā)生于此線程的每一個動作。
比如說主線程 A 啟動子線程 B 后,子線程 B 能夠看到主線程在啟動子線程 B 前的所有操作。
5)線程終止規(guī)則(Thread Termination Rule):線程中的所有操作都先行發(fā)生于對此線程的終止檢測,我們可以通過 Thread 對象的 join() 方法是否結(jié)束、Thread 對象的 isAlive() 的返回值等手段檢測線程是否已經(jīng)終止執(zhí)行。
6)線程中斷規(guī)則(Thread Interruption Rule):對線程 interrupt() 方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測到中斷事件的發(fā)生,可以通過 Thread 對象的 interrupted() 方法檢測到是否有中斷發(fā)生。
7)對象終結(jié)規(guī)則(Finalizer Rule):一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行發(fā)生于它的 finalize() 方法的開始。
8)傳遞性(Transitivity):如果操作 A 先行發(fā)生于操作 B,操作 B 先行發(fā)生于操作 C,那就可以得出操作 A 先行發(fā)生于操作 C 的結(jié)論。
四、“時間上的先發(fā)生” 與 “先行發(fā)生”
上述 8 種規(guī)則中,還不斷提到了時間上的先后,那么,“時間上的先發(fā)生” 與 “先行發(fā)生(Happens-before)” 到底有啥區(qū)別?
一個操作 “時間上的先發(fā)生” 是否就代表這個操作會是“先行發(fā)生” 呢?一個操作 “先行發(fā)生” 是否就能推導(dǎo)出這個操作必定是“時間上的先發(fā)生”呢?
很遺憾,這兩個推論都是不成立的。
舉兩個例子論證一下:
private int value = 0;
// 線程 A 調(diào)用
pubilc void setValue(int value){
this.value = value;
}
// 線程 B 調(diào)用
public int getValue(){
return value;
}
假設(shè)存在線程 A 和 B,線程 A 先(時間上的先后)調(diào)用了 setValue(1),然后線程 B 調(diào)用了同一個對象的 getValue() ,那么線程 B 收到的返回值是什么?
我們根據(jù)上述 Happens-before 的 8 大規(guī)則依次分析一下:
由于兩個方法分別由線程 A 和 B 調(diào)用,不在同一個線程中,所以程序次序規(guī)則在這里不適用;
由于沒有 synchronized 同步塊,自然就不會發(fā)生 lock 和 unlock 操作,所以管程鎖定規(guī)則在這里不適用;
同樣的,volatile 變量規(guī)則,線程啟動、終止、中斷規(guī)則和對象終結(jié)規(guī)則也和這里完全沒有關(guān)系。
因為沒有一個適用的 Happens-before 規(guī)則,所以第 8 條規(guī)則傳遞性也無從談起。
因此我們可以判定,盡管線程 A 在操作時間上來看是先于線程 B 的,但是并不能說 A Happens-before B,也就是 A 線程操作的結(jié)果 B 不一定能看到。所以,這段代碼是線程不安全的。
想要修復(fù)這個問題也很簡單?既然不滿足 Happens-before 原則,那我修改下讓它滿足不就行了。比如說把 Getter/Setter 方法都用 synchronized 修飾,這樣就可以套用管程鎖定規(guī)則;再比如把 value 定義為 volatile 變量,這樣就可以套用 volatile 變量規(guī)則等。
這個例子,就論證了一個操作 “時間上的先發(fā)生” 不代表這個操作會是 “先行發(fā)生(Happens-before)”。
再來看一個例子:
// 以下操作在同一個線程中執(zhí)行 int i = 1; int j = 2;
假設(shè)這段代碼中的兩條賦值語句在同一個線程之中,那么根據(jù)程序次序規(guī)則,“int i = 1” 的操作先行發(fā)生(Happens-before)于 “int j = 2”,但是,還記得 Happens-before 的第 2 條定義嗎?還記得上文說過 JMM 實際上是遵守這樣的一條原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。
所以,“int j=2” 這句代碼完全可能優(yōu)先被處理器執(zhí)行,因為這并不影響程序的最終運行結(jié)果。
那么,這個例子,就論證了一個操作 “先行發(fā)生(Happens-before)” 不代表這個操作一定是“時間上的先發(fā)生”。
這樣,綜上兩例,我們可以得出這樣一個結(jié)論:Happens-before 原則與時間先后順序之間基本沒有因果關(guān)系,所以我們在衡量并發(fā)安全問題的時候,盡量不要受時間順序的干擾,一切必須以 Happens-before 原則為準。
五、Happens-before 與 as-if-serial
綜上,我覺得其實讀懂了下面這句話也就讀懂了 Happens-before 了,這句話上文也出現(xiàn)過幾次:JMM 其實是在遵循一個基本原則,即只要不改變程序的執(zhí)行結(jié)果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎么優(yōu)化都行。
再回顧下 as-if-serial 語義:不管怎么重排序,單線程環(huán)境下程序的執(zhí)行結(jié)果不能被改變。
各位發(fā)現(xiàn)沒有?本質(zhì)上來說 Happens-before 關(guān)系和 as-if-serial 語義是一回事,都是為了在不改變程序執(zhí)行結(jié)果的前提下,盡可能地提高程序執(zhí)行的并行度。只不過后者只能作用在單線程,而前者可以作用在正確同步的多線程環(huán)境下:
- as-if-serial 語義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,Happens-before 關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)果不被改變。
- as-if-serial 語義給編寫單線程程序的程序員創(chuàng)造了一個幻境:單線程程序是按程序的順序來執(zhí)行的。Happens-before 關(guān)系給編寫正確同步的多線程程序的程序員創(chuàng)造了一個幻境:正確同步的多線程程序是按 Happens-before 指定的順序來執(zhí)行的。
以上就是JMM核心概念之Happens-before原則的詳細內(nèi)容,更多關(guān)于JMM Happens-before的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring使用注解方式實現(xiàn)創(chuàng)建對象
這篇文章主要介紹了Spring使用注解方式實現(xiàn)創(chuàng)建對象,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2023-02-02
kill命令在Java應(yīng)用中使用的注意事項小結(jié)
這篇文章主要給大家介紹了關(guān)于kill命令在Java應(yīng)用中使用的注意事項,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2019-06-06
SpringBoot攔截器實現(xiàn)對404和500等錯誤的攔截
本篇文章主要介紹了SpringBoot攔截器實現(xiàn)對404和500等錯誤的攔截,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04

