文章目錄
- 寫在前面
- JMM 的設計
- 總結
- happens-before 的定義
- as-if-serial 語義
- happens-before 規則
- 執行個體1
- 執行個體2
- 執行個體3
- 參考資料
寫在前面
happens-before 是 JMM 最核心的概念。對應 Java 程式員來說,了解 happens-before是了解 JMM 的關鍵。
從 JDK 5 開始,Java 使用新的 JSR-133 記憶體模型(除非特别說明,本文針對的都是JSR-133 記憶體模型)。JSR-133 使用 happens-before 的概念來闡述操作之間的記憶體可見性。
在 JMM 中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必 須要存在 happens-before 關系。這裡提到的兩個操作既可以是在一個線程之内,也可以是在不同線程之間。
與程式員密切相關的 happens-before 規則如下。
- 程式順序規則:一個線程中的每個操作,happens-before 于該線程中的任意後續操作。
- 螢幕鎖規則:對一個鎖的解鎖,happens-before 于随後對這個鎖的加鎖。
- volatile 變量規則:對一個 volatile 域的寫,happens-before 于任意後續對這個volatile 域的讀。
- 傳遞性:如果 A happens-before B,且 B happens-before C,那麼 A happens-before C。
**注意!**兩個操作之間具有 happens-before 關系,并不意味着前一個操作必須要在後一個操作之前執行!happens-before 僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前(the first is visible to and ordered before the second)。
JMM 的設計
首先,讓我們來看 JMM 的設計意圖。從 JMM 設計者的角度,在設計 JMM 時,需要考慮兩個關鍵因素。
程式員對記憶體模型的使用。程式員希望記憶體模型易于了解、易于程式設計。程式員希望基于一個強記憶體模型來編寫代碼。
編譯器和處理器對記憶體模型的實作。編譯器和處理器希望記憶體模型對它們的束縛越少越好,這樣它們就可以做盡可能多的優化來提高性能。編譯器和處理器希望實作一個弱記憶體模型。
由于這兩個因素互相沖突,是以 JSR-133 專家組在設計 JMM 時的核心目标就是找到一個好的平衡點:一方面,要為程式員提供足夠強的記憶體可見性保證;另一方面,對編譯器和處理器的限制要盡可能地放松。下面讓我們來看 JSR-133 是如何實作這一目标的。
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
上面計算圓的面積的示例代碼存在 3 個 happens-before 關系,如下:
- A happens-before B。
- B happens-before C。
- A happens-before C。
在以上 3 個 happens-before 關系中,2 和 3 是必需的,但 1 是不必要的。是以,JMM 把happens-before 要求禁止的重排序分為了下面兩類。
- 會改變程式執行結果的重排序。
- 不會改變程式執行結果的重排序。
JMM 對這兩種不同性質的重排序,采取了不同的政策,如下。
- 對于會改變程式執行結果的重排序,JMM 要求編譯器和處理器必須禁止這種重排序。
- 對于不會改變程式執行結果的重排序,JMM 對編譯器和處理器不做要求(JMM允許這種重排序)
總結
(1)JMM 向程式員提供的 happens-before 規則能滿足程式員的需求。JMM 的happens-before 規則不但簡單易懂,而且也向程式員提供了足夠強的記憶體可見性保證(有些記憶體可見性保證其實并不一定真實存在,比如上面的 A happens-before B)。
(2)JMM 對編譯器和處理器的束縛已經盡可能少。從上面的分析可以看出,JMM 其 實是在遵循一個基本原則:隻要不改變程式的執行結果(指的是單線程程式和正确同步的多線程程式),編譯器和處理器怎麼優化都行。例如,如果編譯器經過細緻的分析後,認定一個鎖隻會被單個線程通路,那麼這個鎖可以被消除。再如,如果編譯器經過細緻的分析後,認定一個 volatile 變量隻會被單個線程通路,那麼編譯器可以把這個 volatile 變量當作一個普通變量來對待。這些優化既不會改變程式的執行結果,又能提高程式的執行效率。
happens-before 的定義
happens-before 的概念最初由 Leslie Lamport 在其一篇影響深遠的論文(《Time,Clocks andthe Ordering of Events in a Distributed System》)中提出。Leslie Lamport 使用happens-before 來定義分布式系統中事件之間的偏序關系(partial ordering)。Leslie Lamport 在這篇論文中給出了一個分布式算法,該算法可以将該偏序關系擴充為某種全序關系。
JSR-133 使用 happens-before 的概念來指定兩個操作之間的執行順序。由于這兩個操作可以在一個線程之内,也可以是在不同線程之間。是以,JMM 可以通過 happens-before 關系向程式員提供跨線程的記憶體可見性保證(如果 A 線程的寫操作 a 與 B 線程的讀操作 b 之間存在 happens-before 關系,盡管 a 操作和 b 操作在不同的線程中執行,但JMM 向程式員保證 a 操作将對 b 操作可見)
《JSR-133:Java Memory Model and Thread Specification》對 happens-before 關系的定義如下:
- 如果一個操作 happens-before 另一個操作,那麼第一個操作的執行結果将對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
- 兩個操作之間存在 happens-before 關系,并不意味着 Java 平台的具體實作必須要按照 happens-before 關系指定的順序來執行。如果重排序之後的執行結果,與按happens-before 關系來執行的結果一緻,那麼這種重排序并不非法(也就是說,JMM 允許這種重排序)。
上面的 1) 是 JMM 對程式員的承諾。從程式員的角度來說,可以這樣了解 happens-before 關系:如果 A happens-before B,那麼 Java 記憶體模型将向程式員保證——A 操作的結果将對 B 可見,且 A 的執行順序排在 B 之前。注意,這隻是 Java 記憶體模型向程式員做出的保證!
上面的 2)是 JMM 對編譯器和處理器重排序的限制原則。正如前面所言,JMM 其 實是在遵循一個基本原則:隻要不改變程式的執行結果(指的是單線程程式和正确同步的多線程程式),編譯器和處理器怎麼優化都行。JMM 這麼做的原因是:程式員對于這兩個操作是否真的被重排序并不關心,程式員關心的是程式執行時的語義不能被改變(即執行結果不能被改變)。是以,happens-before 關系本質上和 as-if-serial 語義是一回事。
as-if-serial 語義
as-if-serial 語義的意思是:不管怎麼重排序(編譯器和處理器為了提高并行度),(單線程)程式的執行結果不能被改變。編譯器、runtime 和處理器都必須遵守 as-if-serial 語義。
- as-if-serial 語義保證單線程内程式的執行結果不被改變,happens-before 關系保證正确同步的多線程程式的執行結果不被改變。
- as-if-serial 語義給編寫單線程程式的程式員創造了一個幻境:單線程程式是按程式的順序來執行的。happens-before 關系給編寫正确同步的多線程程式的程式員創造了一個幻境:正确同步的多線程程式是按 happens-before 指定的順序來執行的。
- as-if-serial 語義和 happens-before 這麼做的目的,都是為了在不改變程式執行結果的前提下,盡可能地提高程式執行的并行度。
happens-before 規則
《JSR-133:Java Memory Model and Thread Specification》定義了如下 happens-before 規則。
- 程式順序規則:一個線程中的每個操作,happens-before 于該線程中的任意後續操作。
- 螢幕鎖規則:對一個鎖的解鎖,happens-before 于随後對這個鎖的加鎖。
- volatile 變量規則:對一個 volatile 域的寫,happens-before 于任意後續對這個volatile 域的讀。
- 傳遞性:如果 A happens-before B,且 B happens-before C,那麼 A happens-before C。
- start()規則:如果線程 A 執行操作 ThreadB.start()(啟動線程 B),那麼 A 線程的ThreadB.start()操作 happens-before 于線程 B 中的任意操作。
- join()規則:如果線程 A 執行操作 ThreadB.join()并成功傳回,那麼線程 B 中的任意操作 happens-before 于線程 A 從 ThreadB.join()操作成功傳回。
執行個體1
下面是 volatile 寫-讀建立的 happens-before 關系圖:

- 1 happens-before 2 和 3 happens-before 4 由程式順序規則産生。由于編譯器和處理器都要遵守 as-if-serial 語義,也就是說,as-if-serial 語義保證了程式順序規則。是以,可以把程式順序規則看成是對 as-if-serial 語義的“封裝”。
- 2 happens-before 3 是由 volatile 規則産生。前面提到過,對一個 volatile 變量的讀,總是能看到(任意線程)之前對這個 volatile 變量最後的寫入。是以,volatile 的這個特性可以保證實作 volatile 規則。
- 1 happens-before 4 是由傳遞性規則産生的。這裡的傳遞性是由 volatile 的記憶體屏障插入政策和 volatile 的編譯器重排序規則共同來保證的。
執行個體2
我們來看 start()規則。假設線程 A 在執行的過程中,通過執行 ThreadB.start()來啟動線程 B;同時,假設線程 A 在執行 ThreadB.start()之前修改了一些共享變量,線程 B在開始執行後會讀這些共享變量。
1 happens-before 2 由程式順序規則産生。2 happens-before 4 由 start()規則産生。根據傳遞性,将有 1 happens-before 4。這實意味着,線程 A 在執行ThreadB.start()之前對共享變量所做的修改,接下來線上程 B 開始執行後都将確定對線程B 可見。
執行個體3
我們來看 join()規則。假設線程 A 在執行的過程中,通過執行 ThreadB.join()來等待線程 B 終止;同時,假設線程 B 在終止之前修改了一些共享變量,線程 A 從ThreadB.join()傳回後會讀這些共享變量。
2 happens-before 4 由 join()規則産生;4 happens-before 5 由程式順序規則産生。