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) -
這段碼是happens-before一致的,但不是真正的順序一致。例如,如果r1看到為x=42的寫入,并且r2看到Y=42的寫入,x和y的值都是42,這是一個數據競態條件的結果。
這里,寫入變量都在讀取變量之前,讀取將看到相關的寫入,這將導致OoTA結果。
注:數據競態可能產生推測的結果,這將最終把自己變成自我實現的預言。OoTA保證是關于秉承因果關系的規則。目前的想法是,因果關系可以避免寫入推測。JMM9旨在尋找OoTA的原因和改進方法,以避免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字段保證到這個“正常”的字段。