Java內存模型程序員
內存模型能夠理解爲在特定的操做協議下,對特定的內存或者高速緩存進行讀寫訪問的過程抽象,不一樣架構下的物理機擁有不同的內存模型,Java虛擬機也有本身的內存模型,即Java內存模型(Java Memory Model, JMM)。編程
在C/C++語言中直接使用物理硬件和操做系統內存模型,致使不一樣平臺下併發訪問出錯。而JMM的出現,可以屏蔽掉各類硬件和操做系統的內存訪問差別,實現平臺一致性,是的Java程序可以「一次編寫,處處運行」。數組
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣底層細節。此處的變量指包括了實例字段、靜態字段和構成數組對象的元素,可是不包括局部變量與方法參數,後者是線程私有的,不會被共享。安全
Java內存模型中規定了全部的變量都存儲在主內存中,每條線程還有本身的工做內存(能夠與前面講的處理器的高速緩存類比),線程的工做內存中保存了該線程使用到的變量到主內存副本拷貝,線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要在主內存來完成。多線程
線程、主內存和工做內存的交互關係以下圖所示。架構
注意:這裏的主內存、工做內存與Java內存區域的Java堆、棧、方法區不是同一層次內存劃分,這二者基本上沒有關係。併發
由上面的交互關係可知,關於主內存與工做內存之間的具體交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步到主內存之間的實現細節,Java內存模型定義瞭如下八種操做來完成(結合下圖):函數
· lock(鎖定):做用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。
· unlock(解鎖):做用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
· read(讀取):做用於主內存變量,把一個變量值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
· load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
· use(使用):做用於工做內存的變量,把工做內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個須要使用變量的值的字節碼指令時將會執行這個操做。
· assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦值給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
· store(存儲):做用於工做內存的變量,把工做內存中的一個變量的值傳送到主內存中,以便隨後的write的操做。
· write(寫入):做用於主內存的變量,它把store操做從工做內存中一個變量的值傳送到主內存的變量中。
Java內存模型還規定了在執行上述八種基本操做時,必須知足以下規則:
· 不容許read和load、store和write操做之一單獨出現
· 不容許一個線程丟棄它的最近assign的操做,即變量在工做內存中改變了以後必須同步到主內存中。
· 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從工做內存同步回主內存中。
· 一個新的變量只能在主內存中誕生,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操做以前,必須先執行過了assign和load操做。
· 一個變量在同一時刻只容許一條線程對其進行lock操做,lock和unlock必須成對出現
· 若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前須要從新執行load或assign操做初始化變量的值
· 若是一個變量事先沒有被lock操做鎖定,則不容許對它執行unlock操做;也不容許去unlock一個被其餘線程鎖定的變量。
· 對一個變量執行unlock操做以前,必須先把此變量同步到主內存中(執行store和write操做)。
這8種內存訪問操做很繁瑣,後文會使用一個等效判斷原則,即先行發生(happens-before)原則來肯定一個內存訪問在併發環境下是否安全。
3.1原子性
JMM要求lock、unlock、read、load、assign、use、store、write這8個操做都必須具備原子性,但對於64位的數據類型(long和double,具備非原子協定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲2次32位操做進行。(與此相似的是,在棧幀結構的局部變量表中,long和double類型的局部變量可使用2個能存儲32位變量的變量槽(Variable Slot)來存儲的,
若是多個線程共享一個沒有聲明爲volatile的long或double變量,而且同時讀取和修改,某些線程可能會讀取到一個既非原值,也不是其餘線程修改值的表明了「半個變量」的數值。不過這種狀況十分罕見。由於非原子協議換句話說,一樣容許long和double的讀寫操做實現爲原子操做,而且目前絕大多數的虛擬機都是這樣作的。
前面分析volatile語義時已經提到,可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。JMM在變量修改後將新值同步回主內存,依賴主內存做爲媒介,在變量被線程讀取前從內存刷新變量新值,保證變量的可見性。普通變量和volatile變量都是如此,只不過volatile的特殊規則保證了這種可見性是當即得知的,而普通變量並不具有這種嚴格的可見性。除了volatile外,synchronized和final也能保證可見性。
JMM的有序性表現爲:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。前半句指「線程內表現爲串行的語義」(as-if-serial),後半句指「指令重排序」和普通變量的」工做內存與主內存同步延遲「的現象。
as-if-serial語義(語義)
as-if-serial語義的意思指:管怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。編譯器,runtime 和處理器都必須遵照as-if-serial語義。
as-if-serial語義把單線程程序保護了起來,遵照as-if-serial語義的編譯器,runtime 和處理器共同爲編寫單線程程序的程序員建立了一個幻覺:單線程程序是按程序的順序來執行的。as-if-serial語義使單線程程序員無需擔憂重排序會干擾他們,也無需擔憂內存可見性問題。
在執行程序時爲了提升性能,編譯器和處理器常常會對指令進行重排序。從硬件架構上來講,指令重排序是指CPU採用了容許將多條指令不按照程序規定的順序,分開發送給各個相應電路單元處理,而不是指令任意重排。重排序分紅三種類型:
· 編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,能夠從新安排語句的執行順序。
· 指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。
· 內存系統的重排序。因爲處理器使用緩存和讀寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。
從Java源代碼到最終實際執行的指令序列,會通過三種重排序。可是,爲了保證內存的可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序。對於編譯器的重排序,JMM會根據重排序規則禁止特定類型的編譯器重排序;對於處理器重排序,JMM會插入特定類型的內存屏障,經過內存的屏障指令禁止特定類型的處理器重排序。這裏討論JMM對處理器的重排序,爲了更深理解JMM對處理器重排序的處理,先來認識一下常見處理器的重排序規則:
其中的N標識處理器不容許兩個操做進行重排序,Y表示容許。其中Load-Load表示讀-讀操做、Load-Store表示讀-寫操做、Store-Store表示寫-寫操做、Store-Load表示寫-讀操做。能夠看出:常見處理器對寫-讀操做都是容許重排序的,而且常見的處理器都不容許對存在數據依賴的操做進行重排序(對應上面數據轉換那一列,都是N,因此處理器不容許這種重排序)。
那麼這個結論對咱們有什麼做用呢?好比第一點:處理器容許寫-讀操做二者之間的重排序,那麼在併發編程中讀線程讀到多是一個未被初始化或者是一個NULL等,出現不可預知的錯誤,基於這點,JMM會在適當的位置插入內存屏障指令來禁止特定類型的處理器的重排序。內存屏障指令一共有4類:
· LoadLoad Barriers:確保Load1數據的裝載先於Load2以及全部後續裝載指令
· StoreStoreBarriers:確保Store1的數據對其餘處理器可見(會使緩存行無效,並刷新到內存中)先於Store2及全部後續存儲指令的裝載
· LoadStoreBarriers:確保Load1數據裝載先於Store2及全部後續存儲指令刷新到內存
· StoreLoadBarriers:確保Store1數據對其餘處理器可見(刷新到內存,而且其餘處理器的緩存行無效)先於Load2及全部後續裝載指令的裝載。該指令會使得該屏障以前的全部內存訪問指令完成以後,才能執行該屏障以後的內存訪問指令。
數據依賴性
數據依賴的準肯定義是:若是兩個操做同時訪問一個變量,其中一個操做是寫操做,此時這兩個操做就構成了數據依賴。
常見的具備這個特性的如i++、i—。若是改變了具備數據依賴的兩個操做的執行順序,那麼最後的執行結果就會被改變。這也是不能進行重排序的緣由。例如:
· 寫後讀:a = 1; b = a;
· 寫後寫:a = 1; a = 2;
· 讀後寫:a = b; b = 1;
重排序遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。可是這裏所說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮。
對於final域,編譯器和處理器要遵照兩個重排序規則。
1)在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用
變量,這兩個操做之間不能重排序。
2)初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。
咱們編寫的程序都要通過優化後(編譯器和處理器會對咱們的程序進行優化以提升運行效率)纔會被運行,優化分爲不少種,其中有一種優化叫作重排序,重排序須要遵照happens-before規則,不能說你想怎麼排就怎麼排,若是那樣豈不是亂了套。
若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在happens-before關係。
happens-before原則定義以下:
1. 若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。
2. 兩個操做之間存在happens-before關係,並不意味着必定要按照happens-before原則制定的順序來執行。若是重排序以後的執行結果與按照happens-before關係來執行的結果一致,那麼這種重排序並不非法。
與程序員密切相關的默認happens-before原則規則以下:
· 程序順序規則:一個線程中的每一個操做,happens- before 於該線程中的任意後續操做。
· 監視器鎖規則:對一個監視器鎖的解鎖,happens- before 於隨後對這個監視器鎖的加鎖。
· volatile變量規則:對一個volatile域的寫,happens-before 於任意後續對這個volatile域的讀。
· 傳遞性:若是A happens- before B,且B happens-before C,那麼A happens- before C。
happen-before原則是JMM中很是重要的原則,它是判斷數據是否存在競爭、線程是否安全的主要依據,保證了多線程環境下的可見性。是JMM提供給程序員的代表視圖和保證。
volatile變量規則
Synchronized是一個比較重量級的操做,對系統的性能有比較大的影響,而volatile是 Java中提供的更輕量級的同步機制,只保證可見性,不能保證原子性。
volatile變量具備2種特性:
· 保證變量的可見性。對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入,這個新值對於其餘線程來講是當即可見的。
· 屏蔽指令重排序:指令重排序是編譯器和處理器爲了高效對程序進行優化的手段,下文有詳細的分析。
volatile語義並不能保證變量的原子性。對任意單個volatile變量的讀/寫具備原子性,但相似於i++、i–這種複合操做不具備原子性
只有知足下面2條規則時,才能使用volatile來保證併發安全,不然就須要加鎖
· 運算結果不依賴當前變量值,或者只有單一的線程修改變量的值(好比計數器的狀況)
· 變量不須要與其餘的狀態變量共同參與不變約束
如:volatile static int start = 3;
volatile static int end = 6;
線程A執行以下代碼:
while (start < end){
//do something
}
線程B執行以下代碼:
start+=3;
end+=3;
這種狀況下,一旦在線程A的循環中執行了線程B,start有可能先更新成6,形成了一瞬間 start == end,從而跳出while循環的可能性。
由於須要在本地代碼中插入許多內存屏蔽指令在屏蔽特定條件下的重排序,volatile變量的寫操做與讀操做相比慢一些,可是其性能開銷比鎖低不少。
原理:
1. 可見性實現:
線程自己並不直接與主內存進行數據的交互,而是經過線程的工做內存來完成相應的操做。這也是致使線程間數據不可見的本質緣由。所以要實現volatile變量的可見性,直接從這方面入手便可。對volatile變量的寫操做與普通變量的主要區別有兩點:
(1)volatile寫的內存語義以下。
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存
(2)volatile讀的內存語義以下。
當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
經過這兩個操做,就能夠解決volatile變量的可見性問題。
2. 有序性實現:
爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。
Volatile語義的加強:
在舊的內存模型中,當A(volatile的操做)和B(非volatile的操做)之間沒有數據依賴關係時,A和B之間就可能被重排序。加強後沒有數據依賴關係也不能重排。
參考
一、http://blog.csdn.net/u011080472/article/details/51337422
二、周志明,深刻理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社
三、AlphaWang博客,http://blog.csdn.net/vking_wang/article/details/8574376