Java內存模型(Java Memory Model,簡稱JMM),即Java虛擬機定義的一種用來屏蔽各類硬件和操做系統的內存訪問差別,以實現讓java程序在各類平臺下都可以達到一致的內存訪問效果的內存模型。本篇文章大體涉及到五個要點:Java內存模型的基礎,主要介紹JMM抽象結構;Java內存模型中內存屏障;Java內存模型中的重排序;happens-before原則;順序一致性內存模型。還有與JMM相關的三個同步原語(synchronized,volatile,final)將另分三篇文章介紹。
在java中,共享變量是指全部存儲在堆內存中的實例字段,靜態字段和數組對象元素,由於堆內存是全部線程共享的數據區。而局部變量,方法定義參數,異常處理參數不會在線程之間共享,它們不存在內存可見性問題,也不會受到Java內存模型的影響。java
Java內存模型決定了一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,Java內存模型定義了線程與主內存之間的抽象關係:線程之間的共享變量存儲主內存中,每一個線程都有一個私有的本地內存,也叫工做內存,本地內存存儲了該線程須要讀/寫的共享變量的副本。本地內存是JMM的一個抽象的概念,其實並不真實存在。Java內存模型的抽象示意圖以下:程序員
從上圖來看,若是線程A和線程B之間要通訊的話,必需要經歷下面的兩個過程:編程
1.線程A把本地內存A中更新過的共享變量刷新到主內存中去。
2.線程B到主內存中去讀取線程A以前已更新過的共享變量。segmentfault
下面經過示意圖說明以上兩個過程:數組
如上圖:假設初始時,X的值爲0,首先線程A要先從主內存中讀取共享變量x的值,並將其副本存儲在本身的本地內存。接着線程A要把共享變量x的值更新爲1,也就是先把本地內存中的x的副本的值更新爲1,而後再把本地內存中剛更新過的共享變量刷新到主內存,此時主內存中共享變量x的值爲1。而後線程A向線程B發送通知:哥們兒,我已更新了共享變量的值。緩存
隨後,線程B接收到線程A發送的通知,也從主內存中讀取共享變量x的值,並將其副本存儲在本身的本地內存,接着線程B也要修改共享變量的值,先將本地內存B中的副本x修改成2,再將本地內存中的x的值刷新到主內存,此時主內存中共享變量x的值就被更新爲了2。安全
從總體上來看,上述的兩個過程實質上是線程A在向線程B發送消息,並且這個通訊過程必需要通過主內存。JMM經過控制主內存與每一個線程的本地內存之間的交互,來爲java程序員提供內存可見性保證。微信
爲了保證內存的可見性,java編譯器在生成指令序列的適當位置會插入內存屏障指令類禁止特定類型的處理器重排序,java內存模型(JMM)把內存屏障指令分爲4類:多線程
重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行重排序的一種手段。重排序分爲3種類型:併發
從java源代碼到最終實際執行的指令序列,會經歷下面3種重排序:
上述1屬於編譯器重排序,編譯器將java源碼編譯成字節碼時進行一次重排序,2和3屬於處理器重排序。這些重排序可能會致使多線程程序出現內存可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序。對於處理器重排序,JMM的處理器重排序規則會要求java編譯器在生成字節碼指令序列時,插入特定類型的內存屏障指令,經過內存屏障指令來禁止特定類型的處理器重排序。
JMM屬於語言級別的內存模型,它確保在不一樣的編譯器和不一樣的處理器平臺上,經過禁止特定類型的編譯器重排序和處理器重排序,爲程序員提供一致的的內存可見性保證。
若是兩個操做訪問同一個共享變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。數據依賴性分爲如下3種類型:
名稱 | 代碼示例 | 說明 |
---|---|---|
寫後讀 | a= 1 ; b = a | 寫一個變量以後,再讀這個位置的變量值 |
寫後寫 | a = 1 ; a = 2 | 寫一個變量以後,再繼續寫這個內存位置的變量 |
讀後寫 | a = b ; b = 1 | 讀一個變量以後,再寫剛讀的這個變量 |
上面的3種狀況,只要重排序兩個操做的執行順序,程序的結果就可能發生改變。
上面介紹過,編譯器和處理器可能會對操做進行重排序。可是編譯器和處理器進行重排序時會遵循數據依賴性規則,只要兩個操做之間具備數據依賴性,那麼編譯器和處理器就不會對這兩個操做進行重排序,編譯器和處理器重排序的原則上是不改變程序的執行結果,從而提升程序執行性能。
這裏所說的數據依賴性規則僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣的處理器之間和不一樣的線程之間的數據依賴性是不被編譯器和處理器考慮的。
as-if-serial語義是指:無論怎麼重排序,單線程的執行結果是不能被改變的。編譯器和處理器都必須遵循as-if-serial語義。
爲了遵循as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。但若是操做之間不存在數據依賴關係,就能夠被編譯器和處理器重排序。
as-if-serial語義把單線程程序給保護了起來,遵循as-if-serial語義的編譯器和處理器共同爲編寫單線程程序的程序員創造了一個幻覺:單線程程序是按程序代碼的前後順序來執行的。as-if-serial語義使程序員在單線程下無需擔憂重排序會影響程序執行結果,也無需擔憂內存可見性問題。
重排序會可能影響多線程程序的執行結果,請看下面的示例代碼:
public class ReorderExample{ int a = 0; boolean flag = false; @Test public void writer(){ a = 1; //1 flag = true; //2 } @Test public void reader(){ if(flag){ //3 int i = a; //4 System.out.print(i); } } }
flag變量是個標記,用來標識變量a是否被寫入。這裏咱們假設有兩個線程A和B,線程A首先執行writer方法,隨後線程B執行reader方法。問題是線程B在執行操做4時,可否看到線程A在操做1對共享變量a的寫入呢?
答案是否認的,並不必定能看到。
因爲操做1和操做2不存在數據依賴關係,編譯器和處理器能夠對這兩個操做重排序;同理,操做3和操做4也不存在數據依賴關係,編譯器和處理器也能夠對這兩個操做重排序。下面咱們先來看下,當操做1和操做2重排序時,會產生什麼效果。程序執行時序圖以下:
如上圖,操做1和操做2作了重排序。程序執行時,線程A首先將標記變量flag寫爲true,隨後線程B讀取這個變量,因爲條件爲真,線程B將讀取共享變量a,而此時,共享變量a尚未被線程A寫入,因此多線程程序的語義就被重排序破壞了。
下面再看下,當操做3和操做4重排序時會產生什麼效果。下面是操做3和操做4重排序後程序的執行時序圖:
在程序中,操做3和操做4存在控制依賴關係。當代碼中存在控制依賴性時,會影響指令序列執行的並行度,爲此,編譯器和處理器會採用猜想執行來克服控制相關性對並行度的影響。以處理器的猜想執行爲例,執行線程B的處理器能夠提早讀取共享變量a,而後會把共享變量a的值保存到一個名爲重排序緩衝(Reorder Buffer,ROD)的硬件緩存中。當操做3的條件爲真時,就把保存到ROB中的共享變量a的值寫入到變量i中。
從上圖中咱們也能夠看出,猜想執行實質上對操做3和操做4作了重排序。重排序破壞了多線程程序的語義。
在單線程程序中,對存在控制依賴的操做重排序,不會改變執行結果(這也是as-if-serial語義容許對存在控制依賴的操做作重排序的緣由);但在多線程中程序中,對存在控制依賴的操做重排序,可能會改變程序的執行結果。
從JDK1.5開始,Java使用新的JSR-133內存模型,該模型使用happens-before原則來闡述操做之間的內存可見性。在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必須存在happens-before關係。這裏提到的兩個操做既能夠是在單線程內,也能夠在多線程之間。
happens-before規則以下:
happens-before與JMM的關係以下圖所示:
順序一致性內存模型是一個理論參考模型,在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型做爲參照。這個內存模型是一個理想化了的理論參考模型。它爲程序員提供了一個極強的內存可見性保證。順序一致性內存模型有兩大特性:
順序一致性內存模型爲程序員提供的視圖以下圖所示:
在概念上,順序一致性模型有一個單一的全局內存,這個內存經過一個左右擺動的開關能夠鏈接到任意一個線程,同時每一個線程必須按照程序的順序來執行內存讀/寫操做。從上面的示意圖能夠看出,在任意時間最多隻能有一個線程能夠鏈接到內存。當多個線程併發執行時,圖中的開關裝置可以把全部 線程的全部內存讀/寫操做串行化。
爲了便於你們更好的理解,下面經過兩個示意圖來對順序一致性模型的特性作進一步的說明。
假設有兩個線程A和B併發執行。其中A線程有三個操做,它們在程序中的順序是:A1 -> A2 -> A3。B線程也有三個操做,它們在程序中的順序是:B1 -> B2 -> B3。
假設這兩個程序使用監視器鎖來正確同步:A線程的三個操做執行完後釋放監視器鎖,隨後B線程獲取同一個監視器鎖。那麼程序在順序一致性模型中的執行效果以下圖所示:
如今咱們再假設這兩個線程沒有作同步,那麼程序在順序一致性模型中的執行效果以下圖所示:
未同步程序在順序一致性模型中雖然總體執行順序是無序的,由於不作同步處理,線程B並不會等到線程A的全部操做都執行完後才執行,而是線程B會和線程A搶佔CPU資源,但全部線程都只能看到一個一致的總體執行順序。以上圖爲例,線程A和線程B看到的執行順序都是:B1 -> A1 ->A2 -> B2 -> A3 -> B3。之因此能獲得這個保證是由於順序一致性內存模型中的每一個操做必須當即對任意線程可見。
可是,在JMM中就沒有這個保證。未同步程序在JMM中不但總體的執行順序是無序的,並且全部線程看到的操做執行順序也可能不一致。好比,當前線程把寫過的數據緩存在本地內存中,在沒有刷新到主內存以前,這個操做僅對當前線程可見;從其餘線程的角度來觀察,會認爲這個寫操做根本沒有被當前線程執行。只有當前線程把本地內存寫過的數據刷新到主內存以後,這個寫操做纔對其餘線程可見。在這種狀況下,當前線程和其餘線程看到的操做執行順序可能不一致。
下面,對前面的示例程序 ReorderExample用鎖來同步,看看正確同步的程序如何具備順序一致性。
public class ReorderExample{ int a = 0; boolean flag = false; @Test public synchronized void writer(){ //獲取鎖 a = 1; flag = true; } //釋放鎖 @Test public synchronized void reader(){ //獲取鎖 if(flag){ int i = a; System.out.print(i); } } //釋放鎖 }
在上面的示例代碼中,假設線程A執行writer方法後,線程B執行reader方法,這是一個正確同步的多線程程序。根據JMM規範,該程序的執行結果將與該程序在順序一致性內存模型中的執行結果相同。下面是該程序在JMM內存模型和順序一致性內存模型中的執行時序對比圖:
順序一致性內存模型中,全部的操做徹底按程序順序串行執行。而在JMM中,臨界區內的代碼指令執行序列能夠被重排序。但JMM不容許臨界區內的代碼逃逸到臨界區以外,那樣會破壞監視器的語義。JMM會在進入臨界區和退出臨界區這兩個關鍵時間點作一些特殊的處理,使得線程在這兩個時間點具備與順序一致性內存模型相同的內存視圖。雖然線程A在臨界區內作了重排序,可是因爲監視器鎖互斥執行的特性,這裏的線程B沒法感知到線程A在臨界區內作了重排序。這種重排序既提升了執行效率,又沒有改變程序的執行結果。
從這裏咱們看到,JMM具備實現上的基本原則爲:在不改變(正確同步的)程序執行結果的前提下,儘量地爲編譯器和處理器的優化打開方便之門。
對於未同步或未正確同步的多線程程序,JMM只提供最小安全性:線程執行時讀取到的值,要麼是以前某個線程寫入的值,要麼就是默認值(0,null,false),JMM保證線程讀操做讀取到的值不會無中生有。爲了實現最小安全性,JMM在堆上爲對象分配內存時,首先會對內存空間進行清零,而後纔會在上面分配對象。所以,在已清零的內存空間分配對象時,域(字段)的默認初始化已經完成了。
JMM不保證未同步或未正確同步的程序的執行結果與該程序在順序一致性內存模型中的執行結果一致。由於若是想要保住執行結果一致,JMM須要禁止大量的編譯器和處理器的優化,這對程序的性能會產生很大的影響。並且,未同步程序在這兩個模型中的執行結果一致也沒有什麼實質的意義。
未同步程序在JMM中執行時,總體上是無序的,其執行結果也是沒法預知的。未同步程序在兩個模型中的執行特性有以下三個方面的差別:
1.順序一致性內存模型保證單線程內的操做是按程序的順序執行,而JMM不保證單線程內的操做是按程序順序執行的。
2.順序一致性內存模型保證全部線程只能看到一致的操做執行順序,而JMM是不保證這一點的。
3.JMM不保證對64位的long和double型變量的寫操做具備原子性,而順序一致性保證對全部的內存讀/寫操做都具備原子性。
第三個差別與處理器總線的工做機制密切相關。在計算機中,數據經過總線在處理器和內存之間傳遞。每次處理器和內存之間的數據傳遞都是經過一系列的步驟來完成的,這一系列的步驟稱之爲總線事務。總線事務包括讀事務和寫事務。讀事務從內存傳送數據處處理器,寫事務從處理器傳送數據到內存,每一個事務會讀/寫內存中的一個或多個物理上連續的字內存空間。這裏的關鍵是,總線會同步試圖併發使用總線的事務。在一個處理器執行總線事務期間,總線會禁止其餘的處理器和IO設備執行內存的讀/寫操做。下面,用示意圖說明總線的工做機制:
由圖可知,假設處理器A,B和C同時向總線發起總線事務,這時總線仲裁會對競爭作出裁決,這裏假設總線在仲裁後斷定處理器A在競爭中獲勝。此時處理器A繼續它的總線事務,而其餘的處理器則要等待處理器A的總線事務完成後才能再次執行內存訪問。假設處理器A執行總線事務期間,處理器B向總線發起了總線事務請求,此時處理器B的總線請求是會被禁止的。
總線的這個工做機制可把全部處理器對內存的訪問以串行化方式來執行。在任意時刻,最多隻容許一個處理器訪問內存。這個特性確保了單個總線事務之中的內存讀/寫操做具備原子性。
在一些32位處理器上,若是要求對64位數據的寫操做具備原子性,會有比較大的開銷。爲了照顧這種處理器,java語言規範鼓勵但不強求JVM對64位的long和double類型的變量的寫操做具備原子性。當JVM在這種處理器上運行時,可能會把64位的long或double類型的變量的寫操做拆分紅兩個32位的寫操做來執行。這兩個32位的寫操做可能會別分配到不一樣的總線事務中執行,此時對這個64位變量的寫操做就不具備原子性。
參考書籍:
1.Java併發編程的藝術:本文主要整理了此書,這本書對Java內存模型的講解已經很透徹,因此將書中內容作了整理。
2.深刻理解Java虛擬機:參考了此書Java內存模型的部分,此書的8個內存交互指令在JSR133,也就是從JDK1.5起就再也不使用了,因此本文再也不介紹。
微信公衆號: