happens-before是什麼?JMM最最核心的概念,看完你就懂了

happens-before是JMM最核心的概念。對應Java程序員來說,理解happens-before是理解JMM的關鍵。

我的併發系列文章,前面三篇學習了 Java併發機制底層實現的三個關鍵要素:volatilesynchronized原子性操作。以及Java內存模型是爲了解決在併發環境下由於 CPU緩存、編譯器和處理器的指令重排序 導致的可見性、有序性問題。 其中重點學習了 volatile 的內存語義,以及JMM是如何定義和實現的,在學習 volatile 內存語義實現原理時我們瞭解到了 JMM 解決指令重排其實是定義了一項 happens-before 規則,今天我們就來一窺究竟,儘量以通俗易懂的話語帶大家學習 happens-before 。

如果還沒有閱讀 前面幾篇關於 Java內存模型學習的文章,建議先翻到文末點擊對應鏈接閱讀,循序漸進比較好一點。

接下來,就進入主題,開始今天的表演。


JMM的設計

要學習 happens-before 這裏首先介紹下JMM的設計意圖。這個問題首先從實際出發:

  1. 我們程序員寫代碼時,是要求內存模型易於理解,易於編程,所以我們需要依賴一個強內存模型來編碼。 也就是說向公理一樣,定義好的規則,我們遵守規則寫代碼就完事了。
  2. 對於編譯器和處理器的實現來說,它們希望約束儘量少一些,畢竟你限制它們肯定影響它們的執行效率,不能讓他們盡己所能的優化來提供性能。所以他們需要一個弱內存模型。

好了,上面談到的這兩點明顯就是衝突的,作爲程序員我們希望JMM提供給我們一個強內存模型,而底層的編譯器和處理器又需要一個弱內存模型來提高自己的性能。

在計算機領域,這種需要權衡的場景非常多,比如內存和CPU寄存器,就引入了CPU多級緩存來解決性能問題,不過也引入了多核cpu併發場景下的各種問題。 所以這裏也一樣,我們需要找到一個平衡點,來滿足程序員的需求,同時也儘可能滿足編譯器和處理器限制放鬆,性能最大化。

因此JMM在設計時,定義瞭如下策略:

  1. 對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
  2. 對於不會改變程序執行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)。

下面結合這副JMM設計圖來理解下:

從上圖中可以看到,JMM向我們程序員提供了足夠強的內存可見性保證,在不影響程序執行結果的情況下,有些可見性保證並一定存在,比如下面的程序,A happens-before B 並不保證,因爲其不影響程序執行結果;

double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

這就引出了另一個方面,JMM爲了滿足編譯器和處理器的約束儘可能少,它遵循的規則是:只要不改變程序的執行結果,編譯器和處理器想怎麼優化就怎麼優化。例如,如果編譯器經過細緻的分析後,認定一個鎖只會被單個線程訪問,那麼這個鎖可以被消除。再如,如果編譯器經過細緻的分析後,認定一個volatile變量只會被單個線程訪問,那麼編譯器可以把這個volatile變量當作一個普通變量來對待。這些優化既不會改變程序的執行結果,又能提高程序的執行效率。

happens-before 規則

如何 理解 happens-before規則呢? 如果僅僅望文生義理解爲先行發生,那麼南轅北轍了。happens-before 表達的並不是說前面一個操作發生在後面一個操作的前面,儘管從程序員編程角度來看也並不會出錯,但它其實表達的是,前一個操作的結果對後續操作時可見的

你可能會問這兩種說法有什麼區別呢?

這是因爲JMM爲程序員提供的視角就是按順序執行的,且滿足一個操作 happens-before 於另一個操作,那麼第一個操作的執行結果將對第二個執行結果可見,而且第一個操作的執行順序排在第二個順序之前。注意,這是 JMM向程序員做出的保證

但其實,JMM在對編譯器和處理器進行約束時,如前面所說,遵循的規則是:再不改變程序執行結果的前提下,編譯器和處理器怎麼優化都行。也就是說兩個操作之間存在 happens-before 規則Java平臺並不一定按照規則定義的順序來執行。 這麼做的原因是因爲,我們程序員並不關心兩個操作是否被重排序,只要保證程序執行時語義不能改變就好了。

happens-before這麼做的目的,都是爲了在不改變程序執行結果的前提下,儘可能地提高程序執行的並行度。

理解了 happens-before 的含義後,我們一起來看下具體的 happens-before 規則定義。

1.程序順序規則

一個線程中,按照程序順序,前面的操作 Happens-Before 於後續的任意操作。這個還是非常好理解的,比如上面那三行代碼,第一行的 "double pi = 3.14; " happens-before 於 “double r = 1.0;”,這就是規則1的內容,比較符合單線程裏面的邏輯思維,很好理解。

double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

2. 監視器鎖規則

對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。

這個規則中說的鎖其實就是Java裏的 synchronized。例如下面的代碼,在進入同步塊之前,會自動加鎖,而在代碼塊執行完會自動釋放鎖,加鎖以及釋放鎖都是編譯器幫我們實現的。

synchronized (this) { //此處自動加鎖
  // x是共享變量,初始值=10
  if (this.x < 12) {
    this.x = 12; 
  }  
} //此處自動解鎖

所以結合鎖規則,可以理解爲:假設 x 的初始值是 10,線程 A 執行完代碼塊後 x 的值會變成 12(執行完自動釋放鎖),線程 B 進入代碼塊時,能夠看到線程 A 對 x 的寫操作,也就是線程 B 能夠看到 x==12。這個也是符合我們直覺的,非常好理解。。

3. volatile變量規則

對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀

這個就有點費解了,對一個 volatile 變量的寫操作相對於後續對這個 volatile 變量的讀操作可見,這怎麼看都是禁用緩存的意思啊,貌似和 1.5 版本以前的語義沒有變化啊(前面講的1.5版本前允許volatile變量和普通變量之間重排序)?如果單看這個規則,的確是這樣,但是如果我們關聯一下規則 4,你就能感受到變化了

4. 傳遞性

如果A happens-before B,且B happens-before C,那麼A happens-before C。

我們將規則 4 的傳遞性應用到我們下面的例子中,會發生什麼呢?

class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 這裏x會是多少呢?
    }
  }
}

可以看下面這幅圖:

從圖中,我們可以看到:

  1. “x=42” Happens-Before 寫變量 “v=true” ,這是規則 1 的內容;
  2. 寫變量“v=true” Happens-Before 讀變量 “v=true”,這是規則 3 的內容 。
  3. 再根據這個傳遞性規則,我們得到結果:“x=42” Happens-Before 讀變量“v=true”。這意味着什麼呢?

如果線程 B 讀到了“v=true”,那麼線程 A 設置的“x=42”對線程 B 是可見的。也就是說,線程 B 能看到 “x == 42” ,有沒有一種恍然大悟的感覺?這就是 1.5 版本對 volatile 語義的增強,這個增強意義重大,1.5 版本的併發工具包(java.util.concurrent)就是靠 volatile 語義來搞定可見性的。

5. start()規則

這條是關於線程啓動的。它是指主線程 A 啓動子線程 B 後,子線程 B 能夠看到主線程在啓動子線程 B 前的操作。

6. join()規則

如果線程A執行操作ThreadB.join()併成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。

通過上面6中 happens-before 規則的組合就能爲我們程序員提供一致的內存可見性。 常用的就是規則1和其他規則結合,爲我們編寫併發程序提供可靠的內存可見性模型。

總結

在 Java 語言裏面,Happens-Before 的語義本質上是一種可見性,A Happens-Before B 意味着 A 事件對 B 事件來說是可見的,無論 A 事件和 B 事件是否發生在同一個線程裏。例如 A 事件發生在線程 1 上,B 事件發生在線程 2 上,Happens-Before 規則保證線程 2 上也能看到 A 事件的發生。

JMM的設計分爲兩部分,一部分是面向我們程序員提供的,也就是happens-before規則,它通俗易懂的向我們程序員闡述了一個強內存模型,我們只要理解 happens-before規則,就可以編寫併發安全的程序了。 另一部分是針對JVM實現的,爲了儘可能少的對編譯器和處理器做約束,從而提高性能,JMM在不影響程序執行結果的前提下對其不做要求,即允許優化重排序。 我們只需要關注前者就好了,也就是理解happens-before規則。畢竟我們是做程序員的,術業有專攻,能寫出安全的併發程序就好了。

 

精彩文章推薦:


死磕Java併發?首先需要學習的併發機制底層實現的三個原理

阿里面試,一面就倒在了Java內存模型上

volatile關鍵字你不瞭解?趕緊來看看

最近面試 字節、BAT,整理一份面試資料《Java 面試 BAT 通關手冊》,覆蓋了 Java 核心技術、JVM、Java 併發、SSM、微服務、數據庫、數據結構等等。獲取方式:點贊後,關注公衆號並回復 666 領取,更多內容陸續奉上

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章