天天看點

happens-before規則——了解happens-before規則

文章目錄

  • ​​寫在前面​​
  • ​​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 關系的定義如下:

  1. 如果一個操作 happens-before 另一個操作,那麼第一個操作的執行結果将對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。
  2. 兩個操作之間存在 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 語義。

  1. as-if-serial 語義保證單線程内程式的執行結果不被改變,happens-before 關系保證正确同步的多線程程式的執行結果不被改變。
  2. as-if-serial 語義給編寫單線程程式的程式員創造了一個幻境:單線程程式是按程式的順序來執行的。happens-before 關系給編寫正确同步的多線程程式的程式員創造了一個幻境:正确同步的多線程程式是按 happens-before 指定的順序來執行的。
  3. as-if-serial 語義和 happens-before 這麼做的目的,都是為了在不改變程式執行結果的前提下,盡可能地提高程式執行的并行度。

happens-before 規則

《JSR-133:Java Memory Model and Thread Specification》定義了如下 happens-before 規則。

  1. 程式順序規則:一個線程中的每個操作,happens-before 于該線程中的任意後續操作。
  2. 螢幕鎖規則:對一個鎖的解鎖,happens-before 于随後對這個鎖的加鎖。
  3. volatile 變量規則:對一個 volatile 域的寫,happens-before 于任意後續對這個volatile 域的讀。
  4. 傳遞性:如果 A happens-before B,且 B happens-before C,那麼 A happens-before C。
  5. start()規則:如果線程 A 執行操作 ThreadB.start()(啟動線程 B),那麼 A 線程的ThreadB.start()操作 happens-before 于線程 B 中的任意操作。
  6. join()規則:如果線程 A 執行操作 ThreadB.join()并成功傳回,那麼線程 B 中的任意操作 happens-before 于線程 A 從 ThreadB.join()操作成功傳回。

執行個體1

下面是 volatile 寫-讀建立的 happens-before 關系圖:

happens-before規則——了解happens-before規則
  1. 1 happens-before 2 和 3 happens-before 4 由程式順序規則産生。由于編譯器和處理器都要遵守 as-if-serial 語義,也就是說,as-if-serial 語義保證了程式順序規則。是以,可以把程式順序規則看成是對 as-if-serial 語義的“封裝”。
  2. 2 happens-before 3 是由 volatile 規則産生。前面提到過,對一個 volatile 變量的讀,總是能看到(任意線程)之前對這個 volatile 變量最後的寫入。是以,volatile 的這個特性可以保證實作 volatile 規則。
  3. 1 happens-before 4 是由傳遞性規則産生的。這裡的傳遞性是由 volatile 的記憶體屏障插入政策和 volatile 的編譯器重排序規則共同來保證的。

執行個體2

我們來看 start()規則。假設線程 A 在執行的過程中,通過執行 ThreadB.start()來啟動線程 B;同時,假設線程 A 在執行 ThreadB.start()之前修改了一些共享變量,線程 B在開始執行後會讀這些共享變量。

happens-before規則——了解happens-before規則

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()傳回後會讀這些共享變量。

happens-before規則——了解happens-before規則

2 happens-before 4 由 join()規則産生;4 happens-before 5 由程式順序規則産生。

參考資料