1. 程式人生 > >java記憶體模型之三--順序一致性

java記憶體模型之三--順序一致性

在順序一致性模型中,所有操作完全按程式的順序序列執行。而在JMM中,臨界區內的程式碼可以重排序(但JMM不允許臨界區內的程式碼“逸出”到臨界區之外,那樣會破壞監視器的語義)。JMM會在退出監視器和進入監視器這兩個關鍵時間點做一些特別處理,使得執行緒在這兩個時間點具有與順序一致性模型相同的記憶體檢視(具體細節後文會說明)。雖然執行緒A在臨界區內做了重排序,但由於監視器的互斥執行的特性,這裡的執行緒B根本無法“觀察”到執行緒A在臨界區內的重排序。這種重排序既提高了執行效率,又沒有改變程式的執行結果。

從這裡我們可以看到JMM在具體實現上的基本方針:在不改變(正確同步的)程式執行結果的前提下,儘可能的為編譯器和處理器的優化開啟方便之門。

未同步程式的執行特性

對於未同步或未正確同步的多執行緒程式,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型變數的讀/寫操作具有原子性,而順序一致性模型保證對所有的記憶體讀/寫操作都具有原子性。

第3個差異與處理器匯流排的工作機制密切相關。在計算機中,資料通過匯流排在處理器和記憶體之間傳遞。每次處理器和記憶體之間的資料傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為匯流排事務(bus transaction)。匯流排事務包括讀事務(read transaction)和寫事務(write transaction)。讀事務從記憶體傳送資料到處理器,寫事務從處理器傳送資料到記憶體,每個事務會讀/寫記憶體中一個或多個物理上連續的字。這裡的關鍵是,匯流排會同步試圖併發使用匯流排的事務。在一個處理器執行匯流排事務期間,匯流排會禁止其它所有的處理器和I/O裝置執行記憶體的讀/寫。下面讓我們通過一個示意圖來說明匯流排的工作機制:


如上圖所示,假設處理器A,B和C同時向匯流排發起匯流排事務,這時匯流排仲裁(bus arbitration)會對競爭作出裁決,這裡我們假設匯流排在仲裁後判定處理器A在競爭中獲勝(匯流排仲裁會確保所有處理器都能公平的訪問記憶體)。此時處理器A繼續它的匯流排事務,而其它兩個處理器則要等待處理器A的匯流排事務完成後才能開始再次執行記憶體訪問。假設在處理器A執行匯流排事務期間(不管這個匯流排事務是讀事務還是寫事務),處理器D向匯流排發起了匯流排事務,此時處理器D的這個請求會被匯流排禁止。

匯流排的這些工作機制可以把所有處理器對記憶體的訪問以序列化的方式來執行;在任意時間點,最多隻能有一個處理器能訪問記憶體。這個特性確保了單個匯流排事務之中的記憶體讀/寫操作具有原子性。

在一些32位的處理器上,如果要求對64位資料的讀/寫操作具有原子性,會有比較大的開銷。為了照顧這種處理器,java語言規範鼓勵但不強求JVM對64位的long型變數和double型變數的讀/寫具有原子性。當JVM在這種處理器上執行時,會把一個64位long/ double型變數的讀/寫操作拆分為兩個32位的讀/寫操作來執行。這兩個32位的讀/寫操作可能會被分配到不同的匯流排事務中執行,此時對這個64位變數的讀/寫將不具有原子性。

當單個記憶體操作不具有原子性,將可能會產生意想不到後果。請看下面示意圖:


如上圖所示,假設處理器A寫一個long型變數,同時處理器B要讀這個long型變數。處理器A中64位的寫操作被拆分為兩個32位的寫操作,且這兩個32位的寫操作被分配到不同的寫事務中執行。同時處理器B中64位的讀操作被拆分為兩個32位的讀操作,且這兩個32位的讀操作被分配到同一個的讀事務中執行。當處理器A和B按上圖的時序來執行時,處理器B將看到僅僅被處理器A“寫了一半“的無效值。