OpenJDK修訂Java記憶體模型

六翁發表於2016-01-07

現有的Java記憶體模型涵蓋了很多Java語言的語義保證。在這篇文章中,我們將深入這些語義,並體會對現有Java記憶體模型更新的動機。


傳統的Java記憶體模型涵蓋了很多Java語言的語義保證。在這篇文章中,我們將重點介紹其中的幾個語義,以更深入地瞭解他們。對於本文中描述的語義,我們還將嘗試體會對現有Java記憶體模型更新的動機。本文中與與JMM未來更新相關的討論,將被稱為JMM9。

1. Java記憶體模型

現有的Java記憶體模型,如JSR133(以下稱為JMM-JSR133)中所定義的,為共享記憶體指定了一致性模型,並且有助於為開發者提供與JMM-JSR133表述一致的定義。JMM-JSR133規範的目標是確保執行緒通過記憶體互動語義的精確定義,以便允許優化並提供清晰的程式設計模型。JMM-JSR133旨在提供定義和語義,使多執行緒程式不僅是正確的,而且是高效能的,並對現有程式碼庫的影響微乎其微。

考慮到這一點,我們來過一下JMM-JSR133中,過分指定或者指定不足的語義保證,同時重點放到社群廣泛討論的,關於我們如何在JMM9對其改進的話題上。

2. JMM9 – 順序一致性 – 資料競態自由問題

JMM-JSR133談到了相對於操作的程式執行。結合有序操作的執行,描述了這些操作之間的關係。在這篇文章中,我們將擴充套件一些這樣的順序和關係,進而討論一下什麼是順序一致的執行。讓我們先從“程式順序”開始。每個執行緒的程式順序是一個總體順序,表示通過該執行緒執行的所有操作的順序。有時候,並不是所有操作都需要按序執行的。因此,有一些關係僅是部分有序的關係。例如,“happens-before”和“synchronized-with”兩個就是部分有序關係。當一個操作發生在另一個操作之前;第一個操作不僅對第二個操作是可見的,而且其順序在第二個操作之前。這兩個操作之間的關係被稱為是happens-before關係。有時,有些特殊操作需要指定順序,他們被稱為“同步操作”。volatile的讀取和寫入、monitor的鎖定和解鎖等都是同步操作的例子。一個同步操作會引起該操作的“synchronized-with”關係。synchronized-with關係是偏序的,這意味著並非所有兩兩的同步操作都包含這個關係之內。所有同步操作的總體順序被稱為“同步順序”,每個執行都有一個同步順序。

現在讓我們談談順序一致的執行。當所有的讀寫操作是總體有序執行時,被認為是順序一致的(SC)。在SC執行中,讀操作總是能看到最後一次寫入特定變數的值。當SC執行表現為沒有“資料競態”時,該程式被認為是資料競態自由(DRF)的。當程式中有兩個不具備happens-before關係順序的訪問,他們訪問的變數相同且至少其中之一是一個寫訪問時,就會發生資料競態。資料競態自由的順序一致(SC for DRF)意味著DRF程式的行為是順序一致的。但是嚴格支援順序一致是以犧牲效能為代價的,大多數系統會對記憶體中的操作重新排序,以提高執行速度,並“隱藏”昂貴操作的延遲。同時,編譯器也會對程式碼重新排序以優化執行。在保證嚴格順序的一致性的場景中,不能進行這些記憶體操作重新排序或程式碼優化,因此效能會受到影響。JMM-JSR133已經使用底層編譯器、高速緩衝儲存器的相互作用和對程式不可見的JIT,合併了鬆散排序限制和任何重新排序。

注:昂貴操作是那些佔用大量的CPU週期來完成、阻止執行流水線

對於JMM9來說,效能是一個重要的考慮因素,而且任何一門程式語言的記憶體模型,理論上,都應該讓開發者可以利用記憶體模型架構上弱有序(weakly-ordered)的優勢。成功的實現和示例是放鬆嚴格的順序,尤其是在弱有序的架構上。

注:弱序是指可以對讀取和寫入重新排序,並且需要顯式的記憶體屏障遏制這種重新排序的架構

3. JMM9 – 無中生有問題

JMM-JSR133另一個主要的語義是對“無中生有”(Out-of Thin Air,OoTA)值的禁止。“happens-before”模型有時會建立變數值並“無中生有”地讀取,因為它不包含因果條件。有一點非常重要,由自身引起的關係不會採用資料和控制依賴的概念,我們將在下面正確同步程式碼的例子看到,非法寫入是由寫入本身引起的。

(注:x和y初始化為`0`) –

Thread a Thread b
r1 = x; r2 = y;
if (r1 != 0) if (r2 != 0)
y = 42; x = 42;

這段碼是happens-before一致的,但不是真正的順序一致。例如,如果r1看到為x=42的寫入,並且r2看到Y=42的寫入,x和y的值都是42,這是一個資料競態條件的結果。

r1 = x;
y = 42;
r2 = y;
x = 42;

這裡,寫入變數都在讀取變數之前,讀取將看到相關的寫入,這將導致OoTA結果。

注:資料競態可能產生推測的結果,這將最終把自己變成自我實現的預言。OoTA保證是關於秉承因果關係的規則。目前的想法是,因果關係可以避免寫入推測。JMM9旨在尋找Oo他的原因和改進方法,以避免OoTA。

為了禁止OoTA值,一些寫入需要等待他們的讀取來避免資料競態。因此,JMM-JSR133定義的OoTA禁止正式拒絕OoTA讀取。這個正式的定義包括記憶體模型的“執行和因果條件”。基本上,當所有的程式操作提交時,一個良好的執行要滿足因果條件。

注:在每次讀取可以看到對同一變數的寫入時,一個良好的執行遵循在一個執行緒內、“happens-before”和“synchronization-order”一致的執行。

正如你可能已經知道的,JMM-JSR133定義嚴格定義,不讓OoTA值侵襲。JMM9旨在發現和糾正正式的定義,以便允許一些常見的優化。

4. JMM9 非Volatile變數上的Volatile操作

首先,關鍵字`Volatile`是什麼意思呢?Java的‘volatile’保證了執行緒間的互動,使得當一個執行緒寫入一個volatile變數,不僅這次寫入對其他執行緒可見,而且其他執行緒可以看到該執行緒所有的對volatile變數的寫入。

那麼對於non-volatile變數又發生了什麼呢?非volatile變數沒有‘volatile’關鍵字保證互動的好處。因此,編譯器可以使用non-volatile變數的快取值而不是‘volatile’保證,‘volatile’變數將總是從記憶體中讀取。happens-before模型可以用來繫結同步訪問到非volatile變數上。

注:宣告的任何欄位為‘volatile’並不意味著有鎖參與。因此volatile比使用鎖來同步更便宜。但是著重要注意的是,當方法中有多個volatile欄位時,可能比使用鎖更昂貴。

5. JMM9 – 讀寫原子性問題和字分裂問題

JMM-JSR133也有為共享記憶體並行演算法提供的讀取和寫入的原子性保證(使用異常)。異常是為non-volatile的長整型和雙精度浮點型的寫入被視為兩個獨立的寫入而定義的。因此,一個64位的值可以分別寫入兩個32位,一個執行緒正在執行讀的時候,如果其中的一個寫入仍未完成,該執行緒可能會看到只有一半正確的值,從而失去原子性。這是原子性保證依賴於底層硬體和記憶體子系統的一個例子。例如,底層彙編指令應該能夠處理的運算元的大小,以便保證原子性,否則如果讀或寫操作必須被分成多於一個的操作,最終將破壞原子性(正如例子中的non-volatile的長整型和雙精度浮點型的值)。類似地,如果因為實現產生一個以上的記憶體子系統事務,那麼也將破壞原子性。

注:volatile的長整型和雙精度浮點型欄位和引用始終保證讀取和寫入的原子性

基於位的設計不是一個理想的解決方案,因為如果64位的異常被刪除,那麼在32位的體系結構中就會受損。如果在64位架構上行不通,如果期望原子性,那麼不得不為長整型和雙精度浮點型引入“volatile”,即使底層硬體可以保證原子操作。例如:volatile型別的欄位不需要定義為雙精度浮點型,因為基礎架構,或者ISA、浮點單元會處理好64位寬欄位的原子性需求。JMM9的目的是確定硬體提供原子性的保證。

JMM-JSR133寫於十多年前;此後處理器位數發生了演變,64位已經成為主流的處理位數。當即強調的是,JMM-JSR133提出了針對64位讀寫的妥協,儘管64位的值可以由任何架構原子生成,一些架構仍然有必要請求鎖。現在,這使得在這些架構上的64位讀寫操作非常昂貴。在32位x86架構上,如果不能找到一個合理的原子64位操作實現,則原子性將不會改變。

注:在語言設計中潛在一個問題,關鍵字“volatile”被賦予了過分的含義。執行時很難弄清楚,使用者使用“volatile”是為了恢復原子性(因此它可以在64位平臺被剝離出來),還是為了記憶體排序的目的。

當談論訪問原子性,讀寫操作的獨立性是要著重考慮的。寫入一個特定的欄位不應該與讀取或者寫入其他欄位有互動。JMM-JSR133的保證意味著,同步不應需要提供順序一致性。因此,JMM-JSR133保證禁止被稱為“字分裂”的問題。基本上,當更新一個運算元希望在比基礎架構為所有運算元生成的更低的粒度上操作時,我們將遇到“字撕裂”問題。需要記住的重要一點是,字撕裂問題的原因之一是,64位長整型和雙精度浮點型都沒有給出原子性保證。字撕裂在JMM-JSR133中是禁止的,在JMM9中繼續保持這種方式。

6. JMM9 – final欄位問題

與其他欄位相比,final欄位是不同的。例如,一個執行緒用final欄位`x`讀取一個“完全初始化”的物件;在物件“完全初始化”後,能保證讀取了final欄位`y`的初始值值,但不能保證“正常”的非final欄位`nonX`。

注:“完全初始化”是指物件的建構函式完成

鑑於上述情況,有一些簡單的事情可以在JMM9中修復。例如:volatile型別欄位,volatile欄位在建構函式中初始化是不保證可見性的,即使對例項本身是可見的。因此,問題來了,是否final欄位應該保證擴大到所有欄位,包括初始化volatile欄位?此外,如果一個完全初始化物件的“正常”非final欄位的值不發生變化,我們是否可以將final欄位保證到這個“正常”的欄位。

參考文獻

我從如下這些網站學到了很多,他們提供了大量的示例編碼。本文是一篇介紹性的文章,以下文章更適合深入掌握Java記憶體模型。

  1. JSR 133: JavaTM Memory Model and Thread Specification Revision
  2. The Java Memory Model
  3. JAVA CONCURRENCY (&C)
  4. The jmm-dev Archives
  5. Threads and Locks
  6. Synchronization and the Java Memory Model
  7. All Accesses Are Atomic
  8. Java Memory Model Pragmatics (transcript)
  9. Memory Barriers: a Hardware View for Software Hackers

特別感謝

感謝Jeremy Manson,幫助我糾正了很多誤解,併為我更清楚地解釋了那些對於我來說很新的術語。還要感謝Aleksey Shipilev,幫助我減少了本文草稿版本中出現的概念的複雜性。Aleksey還指導我們去他的JMM,語用學文章更深層次的理解,澄清和例子。

關於作者

Monica Beckwith是Java效能顧問。她過去曾經與Oracle/Sun和AMD一起工作,對JVM伺服器級系統進行優化。Monica被評為JavaOne 2013的明星演講者,並且是First Garbage Collector(G1 GC)效能團隊的領導者。她的Twitter是@mon_beck。

The OpenJDK Revised Java Memory Model


相關文章