JMM與順序一致模型和happens-before模型的關係介紹

Java內存模型的設計,參考了順序一致性內存模型和原始的happens-before內存模型,吸收了他們的優點,改進了他們的缺點。下面爲大家講講具體的Java內存模型和二者之間有什麼區別和聯繫。

1 順序一致的內存模型

1.1 數據競爭與順序一致性保證

  多線程下,當程序未正確同步時,就會存在數據競爭。java 內存模型規範對數據競爭的定義如下:在一個線程中寫一個變量;在另一個線程讀同一個變量;而且寫和讀沒有通過同步來排序!
  當代碼中包含數據競爭時,程序的執行往往產生違反直覺的結果(前一章的示例正是如此)。如果一個多線程程序能正確同步,這個程序將是一個沒有數據競爭的程序。
  JMM 對正確同步的多線程程序的內存一致性做了如下保證:
  **如果程序是正確同步的,程序的執行將具有順序一致性(sequentially consistent)-- 即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同。**這裏的同步是指廣義上的同步,包括對常用同步原語(lock,volatile 和 final)的正確使用。下面來看看順序一致性內存模型。

1.2 順序一致性內存模型

  順序一致性內存模型(Sequential Consistency Memory Model)是一個被計算機科學家理想化了的理論參考模型,是程序執行過程中可見性和順序的強有力保證。在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型作爲參照。
  順序一致性內存模型有兩大特性:

  1. 一個線程中的所有操作必須按照程序的順序來執行。
  2. (不管程序是否同步)所有線程都只能看到一個單一的操作執行順序。在順序一致性內存模型中,每個操作都必須原子執行且立刻對所有線程可見。

  順序一致性內存模型爲程序員提供的視圖如下:
在這裏插入圖片描述
  在概念上,順序一致性模型有一個單一的全局內存,這個內存通過一個左右擺動的開關可以連接到任意一個線程,同時每一個線程必須按照程序的順序來執行內存讀/寫操作。在任意時間點最多只能有一個線程可以連接到內存。當多個線程併發執行時,開關裝置能把所有線程的所有內存讀/寫操作串行化。
  假設這兩個線程使用監視器鎖來正確同步:A 線程的三個操作執行後釋放監視器鎖,隨後 B 線程獲取同一個監視器鎖。那麼程序在順序一致性模型中的執行效果如下:
在這裏插入圖片描述
  現在再假設這兩個線程沒有做同步,下面是這個未同步程序在順序一致性模型中的執行示意圖:
在這裏插入圖片描述
  未同步程序在順序一致性模型中雖然整體執行順序是無序的,但所有線程都只能看到一個一致的整體執行順序:以上圖爲例,線程A和B看到的執行順序都是:B1->A1->A2->B2->A3->B3。之所以能得到這個保證是因爲順序一致性內存模型中的每個操作必須立即對任意線程可見。
  但是,在 JMM 中就沒有這個保證。未同步程序在 JMM 中不但整體的執行順序是無序的,而且所有線程看到的操作執行順序也可能不一致。比如,在當前線程把寫過的數據緩存在本地內存中,在還沒有刷新到主內存之前,這個寫操作僅對當前線程可見。
  從其他線程的角度來觀察,會認爲這個寫操作根本還沒有被當前線程執行。只有當前線程把本地內存中寫過的數據刷新到主內存之後,這個寫操作才能對其他線程可見。在這種情況下,當前線程和其它線程看到的操作執行順序將不一致。

1.3 同步程序的順序一致性效果

class SynchronizedExample {
int a = 0;
boolean flag = false;

public synchronized void writer() {
    a = 1;
    flag = true;
}

public synchronized void reader() {
    if (flag) {
        int i = a;
        ……
    }
}
}

  上面示例代碼中,假設A線程執行writer()方法後,B線程執行reader()方法。這是一個正確同步的多線程程序。根據JMM規範,該程序的執行結果將與該程序在順序一致性模型中的執行結果相同。下面是該程序在兩個內存模型中的執行時序對比圖:
在這裏插入圖片描述
  在順序一致性模型中,所有操作完全按程序的順序串行執行。而在JMM中,臨界區內的代碼可以重排序(但JMM不允許臨界區內的代碼“逸出”到臨界區之外,那樣會破壞監視器的語義)。JMM會在退出監視器和進入監視器這兩個關鍵時間點做一些特別處理,使得線程在這兩個時間點具有與順序一致性模型相同的內存視圖(具體細節後文會說明)。雖然線程A在臨界區內做了重排序,但由於監視器的互斥執行的特性,這裏的線程B根本無法“觀察”到線程A在臨界區內的重排序。這種重排序既提高了執行效率,又沒有改變程序的執行結果。
  從這裏我們可以看到JMM在具體實現上的基本方針:在不改變(正確同步的)程序執行結果的前提下,儘可能的爲編譯器和處理器的優化打開方便之門。

2.4 未同步程序的執行特性

  對於未同步或未正確同步的多線程程序,JMM 只提供最小安全性:線程執行時讀取到的值,要麼是之前某個線程寫入的值,要麼是默認值(0,null,false),JMM 保證線程讀操作讀取到的值不會無中生有(out of thin air)的冒出來。爲了實現最小安全性,JVM 在堆上分配對象時,首先會清零內存空間,然後纔會在上面分配對象(JVM 內部會同步這兩個操作)。因此,在以清零的內存空間(pre-zeroed memory)分配對象時,域的默認初始化已經完成了。
  JMM 不保證未同步程序的執行結果與該程序在順序一致性模型中的執行結果一致。因爲未同步程序在順序一致性模型中執行時,整體上是無序的,其執行結果無法預知。保證未同步程序在兩個模型中的執行結果一致毫無意義。
  和順序一致性模型一樣,未同步程序在 JMM 中的執行時,整體上也是無序的,其執行結果也無法預知。同時,未同步程序在這兩個模型中的執行特性有下面幾個差異:

  1. 順序一致性模型保證單線程內的操作會按程序的順序執行,而 JMM 不保證單線程內的操作會按程序的順序執行(比如上面正確同步的多線程程序在臨界區內的重排序)。這一點前面已經講過了,這裏就不再贅述。
  2. 順序一致性模型保證所有線程只能看到一致的操作執行順序,而 JMM 不保證所有線程能看到一致的操作執行順序。這一點前面也已經講過,這裏就不再贅述。
  3. JMM 不保證對 64 位的 long 型和 double 型變量的讀 / 寫操作具有原子性,而順序一致性模型保證對所有的內存讀 / 寫操作都具有原子性。

2 原始的happens-before內存模型

2.1 因果關係問題

  原始的happens-before模型太弱了,所有Java內存模型允許的行爲,happens-before內存模型也允許,但是有些行爲是Java內存模型不允許的,比如以不可捉摸的方法違反因果關係——允許某些值憑空出現。如下圖一:
在這裏插入圖片描述
  如上代碼,在正確同步的原始的happens-before內存模型中,存在執行結果是r1 = r2 = 1的情況。因爲在原始的happens-before內存模型中,代碼可能被優化爲:
在這裏插入圖片描述
  這就導致了不可能出現的結果r1 = r2 = 1出現。
  當一個寫操作發生在了一個其依賴的讀操作之前,我們將這樣的問題稱爲因果關係,因爲它涉及寫操作是否會觸發自身發生的問題。讀操作促使寫操作發生,然後寫操作使得讀操作能看到它們都看到的值。

2.2 Java內存模型的改進

  Java內存模型將一個特定的執行過程和一個程序作爲輸入,然後確定該執行過程是否是該程序的一次合法執行。它是通過逐步地建立一組“提交的”動作來實現的,這些動作反映出了我們知道的哪些動作能夠被程序執行而不需要一個“因果循環”。
  通常,下一個將要提交的動作表示的是能被順序一致的執行過程執行的下一個動作。然而,爲了表明讀操作能看到程序順序裏後面其它線程寫的值,我們允許一些動作比更早發生的其它動作先提交。
  Java內存模型允許下圖二中的行爲,即使該例子看似也存在循環因果關係。必須允許這樣的行爲,因爲編譯器能夠:消除多餘的讀取a的操作,將r2 = a替換爲r2 = r1,然後,確定了表達式r1 == r2總是爲true,消除條件分支3,然後最終將4: b = 2移到前面。這對編譯器的性能提升是很有幫助的。
在這裏插入圖片描述
  有些動作可以提前提交,有些則不能。Java內存模型不允許最開始圖一中r1 = r2 = 1的情況發生,但是允許圖二的替換情況發生。Java內存模型會判斷某個動作的發生不會產生數據爭用,就允許該動作提前提交。
  所謂數據爭用(data race),簡單的解釋就是:一個線程裏有個寫操作,另一個線程讀取了這個寫入的變量值,且讀寫操作沒有被同步排序。當上述情況發生時,稱之爲存在數據爭用。
  對於圖一由於不知道爭用數據x的值,因此Java內存模型不允許寫操作發生在讀之前;對於圖二,在Jvav內存模型看來。雖然不知道a的值,但是r1==r2始終爲true,因此允許優化!

3 總結

  順序一致性內存模型對於Java來說,它太嚴格了,不適合做Java內存模型,因爲它禁止了標準的編譯器和處理器優化,影響性能。然後是原始happens-before內存模型,這個模型已經非常接近Java內存模型的需求,但是,它太弱了,其允許違反因果關係這種不可接受的事情發生,這對於Java內存模型和程序員來說都是不可接受的。因此Java使用了改良後的 happens-before內存模型,形成了自己跌內存模型,即JMM。
  關於JMM和改良後的happens-before內存模型,詳情可見這篇文章:Java內存模型與happens-before原則詳解

參考
《JSR133規範》
《Java併發編程的藝術》
《Java併發編程之美》

如果有什麼不懂或者需要交流,可以留言。另外希望點贊、收藏、關注,我將不間斷更新各種Java學習博客!

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