傳統的Java內存模型涵蓋了很多Java語言的語義保證。在這篇文章中,我們將重點介紹其中的幾個語義,以更深入地了解他們。對於本文中描述的語義,我們還將嘗試體會對現有Java內存模型更新的動機。本文中與JMM未來更新相關的討論,將被稱為JMM9。
現有的Java內存模型,如JSR133(以下稱為JMM-JSR133)中所定義的,為共享內存指定了一致性模型,並且有助於為開發者提供與JMM-JSR133表述一致的定義。JMM-JSR133規范的目標是確保線程通過內存交互語義的精確定義,以便允許優化並提供清晰的編程模型。JMM-JSR133旨在提供定義和語義,使多線程程序不僅是正確的,而且是高性能的,並對現有代碼庫的影響微乎其微。
考慮到這一點,我們來過一下JMM-JSR133中,過分指定或者指定不足的語義保證,同時重點放到社區廣泛討論的,關於我們如何在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)的優勢。成功的實現和示例是放松嚴格的順序,尤其是在弱有序的架構上。
注:弱序是指可以對讀取和寫入重新排序,並且需要顯式的內存屏障遏制這種重新排序的架構。
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旨在尋找OoTA的原因和改進方法,以避免OoTA。
為了禁止OoTA值,一些寫入需要等待他們的讀取來避免數據競態。因此,JMM-JSR133定義的OoTA禁止正式拒絕OoTA讀取。這個正式的定義包括內存模型的“執行和因果條件”。基本上,當所有的程序操作提交時,一個良好的執行要滿足因果條件。
注:在每次讀取可以看到對同一變量的寫入時,一個良好的執行遵循在一個線程內、happens-before和synchronization-order一致地執行。
正如你可能已經知道的,JMM-JSR133定義嚴格定義,不讓OoTA值侵襲。JMM9旨在發現和糾正正式的定義,以便允許一些常見的優化。
首先,關鍵字'Volatile'是什麼意思呢?Java的volatile保證了線程間的交互,使得當一個線程寫入一個volatile變量,不僅這次寫入對其他線程可見,而且其他線程可以看到該線程所有的對volatile變量的寫入。
那麼對於non-volatile變量又發生了什麼呢?非volatile變量沒有volatile關鍵字保證交互的好處。因此,編譯器可以使用non-volatile變量的緩存值而不是volatile保證,volatile變量將總是從內存中讀取。happens-before模型可以用來綁定同步訪問到非volatile變量上。
注:聲明的任何字段為volatile並不意味著有鎖參與。因此volatile比使用鎖來同步更便宜。但是著重要注意的是,當方法中有多個volatile字段時,可能比使用鎖更昂貴。
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中繼續保持這種方式。
與其他字段相比,final字段是不同的。例如,一個線程用final字段x讀取一個“完全初始化”的對象;在對象“完全初始化”後,能保證讀取了final字段y的初始值,但不能保證“正常”的非final字段nonX。
注:“完全初始化”是指對象的構造函數完成。
鑒於上述情況,��一些簡單的事情可以在JMM9中修復。例如:volatile類型字段,volatile字段在構造函數中初始化是不保證可見性的,即使對實例本身是可見的。因此,問題來了,是否final字段應該保證擴大到所有字段,包括初始化volatile字段?此外,如果一個完全初始化對象的“正常”非final字段的值不發生變化,我們是否可以將final字段保證到這個“正常”的字段。
我從如下這些網站學到了很多,他們提供了大量的示例編碼。本文是一篇介紹性的文章,以下文章更適合深入掌握Java內存模型。
感謝Jeremy Manson,幫助我糾正了很多誤解,並為我更清楚地解釋了那些對於我來說很新的術語。還要感謝Aleksey Shipilev,幫助我減少了本文草稿版本中出現的概念的復雜性。Aleksey還指導我們去他的JMM,語用學文章更深層次的理解,澄清和例子。
CentOS 6.5上編譯安裝OpenJDK7源碼 http://www.linuxidc.com/Linux/2015-05/117248.htm
RHEL6.5安裝OpenJDK1.7.0 + JBoss7.1.1 + Maven3.0.4 http://www.linuxidc.com/Linux/2014-04/99854.htm
Fedora 20下安裝官方JDK替換OpenJDK並配置環境變量 http://www.linuxidc.com/Linux/2014-03/97523.htm
Ubuntu OpenJDK + Tomcat7 的安裝 http://www.linuxidc.com/Linux/2014-02/96398.htm
Ubuntu 13.04下升級到Maven3.10 以支持 OpenJDK7 http://www.linuxidc.com/Linux/2013-08/88844.htm
Ubuntu 12.10中編譯安裝OpenJDK 7 http://www.linuxidc.com/Linux/2013-03/81948.htm
Monica Beckwith是Java性能顧問。她過去曾經與Oracle/Sun和AMD一起工作,對JVM服務器級系統進行優化。Monica被評為JavaOne 2013的明星演講者,並且是First Garbage Collector(G1 GC)性能團隊的領導者。她的Twitter是@mon_beck。
查看英文原文:The OpenJDK Revised Java Memory Model