Java內存模型總結

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內存模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

·        不允許readloadstorewrite操作之一單獨出現

·        不允許一個線程丟棄它的最近assign的操作,即變量在工作內存中改變了之後必須同步到主內存中。

·        不允許一個線程無原因地(沒有發生過任何assign操作)把數據從工作內存同步回主內存中。

·        一個新的變量只能在主內存中誕生,不允許在工作內存中直接使用一個未被初始化(loadassign)的變量。即就是對一個變量實施usestore操作之前,必須先執行過了assignload操作。

·        一個變量在同一時刻只允許一條線程對其進行lock操作,lockunlock必須成對出現

·        如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行loadassign操作初始化變量的值

·        如果一個變量事先沒有被lock操作鎖定,則不允許對它執行unlock操作;也不允許去unlock一個被其他線程鎖定的變量。

·        對一個變量執行unlock操作之前,必須先把此變量同步到主內存中(執行storewrite操作)。

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的讀寫操作實現爲原子操作,並且目前絕大多數的虛擬機都是這樣做的。

3.2可見性

前面分析volatile語義時已經提到,可見性是指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。JMM在變量修改後將新值同步回主內存,依賴主內存作爲媒介,在變量被線程讀取前從內存刷新變量新值,保證變量的可見性。普通變量和volatile變量都是如此,只不過volatile的特殊規則保證了這種可見性是立即得知的,而普通變量並不具備這種嚴格的可見性。除了volatile外,synchronized和final也能保證可見性。

3.3有序性

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採用了允許將多條指令不按照程序規定的順序,分開發送給各個相應電路單元處理,而不是指令任意重排。重排序分成三種類型:

·        編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序。

·        指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。

·        內存系統的重排序。由於處理器使用緩存和讀寫緩衝區,這使得加載和存儲操作看上去可能是在亂序執行。

JMM的重排序屏障

從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關係。

 

 

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

 

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的循環中執行了線程Bstart有可能先更新成6,造成了一瞬間 start == end,從而跳出while循環的可能性。

 

因爲需要在本地代碼中插入許多內存屏蔽指令在屏蔽特定條件下的重排序,volatile變量的寫操作與讀操作相比慢一些,但是其性能開銷比鎖低很多。

 

 

 

 

原理:

1.      可見性實現:

線程本身並不直接與主內存進行數據的交互,而是通過線程的工作內存來完成相應的操作。這也是導致線程間數據不可見的本質原因。因此要實現volatile變量的可見性,直接從這方面入手即可。對volatile變量的寫操作與普通變量的主要區別有兩點:

1volatile寫的內存語義如下。
當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存

2volatile讀的內存語義如下。
當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

通過這兩個操作,就可以解決volatile變量的可見性問題。

 

2.      有序性實現:

爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

 

Volatile語義的增強:

在舊的內存模型中,當Avolatile的操作)和B(非volatile的操作)之間沒有數據依賴關係時,AB之間就可能被重排序。增強後沒有數據依賴關係也不能重排。

 

 

參考 

1、http://blog.csdn.net/u011080472/article/details/51337422
2、周志明,深入理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社 
3、AlphaWang博客,http://blog.csdn.net/vking_wang/article/details/8574376

版權聲明:本文爲博主原創文章,轉載請註明作者和出處。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章