Java虛擬機16:Java內存模型

什麼是Java內存模型html

Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽掉各類硬件和操做系統的訪問差別,以實現讓Java程序在各類平臺下都能達到一致的內存訪問效果。在此以前,主流程序語言(如C/C++等)直接使用物理硬件和操做系統的內存模型,所以,會因爲不一樣平臺上內存模型的差別,有可能致使程序在一套平臺上併發徹底正常,而在另一套平臺上併發訪問卻常常出錯,所以在某些場景下就不準針對不一樣的平臺來編寫程序。數組

Java內存模型即要定義得足夠嚴謹,才能讓Java的併發內存訪問操做不會產生歧義;Java內存模型也必須定義地足夠寬鬆,才能使得虛擬機的實現有足夠的自由空間去利用硬件的各類特性來獲取更好的執行速度。通過長時間的驗證和修補,JDK1.5(實現了JSR-133)發佈以後,Java內存模型已經成熟和完善起來了,一塊兒來看一下。安全

 

主內存和工做內存併發

Java內存模型的主要目的是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。注意一下,此處的變量並不包括局部變量與方法參數,由於它們是線程私有的,不會被共享,天然也不會存在競爭,此處的變量應該是實例字段、靜態字段和構成數組對象的元素。app

Java內存模型規定了全部的變量都存儲在主內存(Main Memory)中,每條線程還有本身的工做內存(Working Memory),線程的工做內存中保存了被該線程使用到的變量和主內存副本拷貝(注意這裏毫不會是整個對象的拷貝,試想一個10M的對象,在每一個用到這個對象的工做內存中有一個10M的拷貝,內存還受得了?也就是一些在線程中用到的對象中的字段罷了),線程對變量全部的操做(讀取、賦值)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主內存來完成,線程、主內存、工做內存三者的交互關係如圖:優化

 

 

內存間相互交互this

關於主內存與工做內存之間具體的交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節,Java內存模型中定義瞭如下8種操做來完成,虛擬機實現時必須保證下面體積的每一種操做都是原子的、不可再分的:spa

一、lock(鎖定):做用於主內存中的變量,它把一個變量標識爲一條線程獨佔的狀態操作系統

二、unlock(解鎖):做用於主內存中的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定線程

三、read(讀取):做用於主內存中的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用

四、load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中

五、use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,沒當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做

六、assign(賦值):做用於工做內存中的變量,它把一個從執行引擎接收到的值賦值給工做內存中的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做

七、store(存儲):做用於工做內存中的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用

八、write(寫入):做用於主內存中的變量,它把store操做從工做內存中獲得的變量值放入主內存的變量中

Java內存模型還規定了在執行上述8種基本操做時必須知足如下規則:

一、不容許read和load、store和write操做之一單獨出現

二、不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了滯後必須把該變化同步回主內存

三、不容許一個線程無緣由地把數據從線程的工做內存同步回主內存中

四、一個新的變量只能從主內存中誕生,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量

五、一個變量在同一時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖

六、若是對同一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值

七、若是一個變量事先沒有被lock操做鎖定,那就不容許對它進行unlock操做,也不容許去unlock一個被其餘線程鎖定的變量

八、對一個變量執行unlock操做以前,必須先把此變量同步回主內存中

 

volatile型變量的特殊規則

關鍵字volatile能夠說是Java虛擬機提供的最輕量級的同步機制。

一個變量被定義爲volatile後,它將具有兩種特性:

一、保證此變量對全部線程的"可見性",所謂"可見性"是指當一條線程修改了這個變量的值,新值對於其它線程來講都是能夠當即得知的,而普通變量不能作到這一點,普通變量的值在在線程間傳遞均須要經過主內存來完成,關於volatile關鍵字的操做請參見volatile關鍵字使用舉例,再強調一遍,volatile只保證了可見性,並不保證基於volatile變量的運算在並罰下是安全的

二、使用volatile變量的第二個語義是禁止指令重排序優化,普通變量僅僅會保證在該方法的執行過程當中全部依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操做的順序與程序代碼中的執行順序一致。

總結一下Java內存模型對volatile變量定義的特殊規則:

一、在工做內存中,每次使用某個變量的時候都必須線從主內存刷新最新的值,用於保證能看見其餘線程對該變量所作的修改以後的值

二、在工做內存中,每次修改完某個變量後都必須馬上同步回主內存中,用於保證其餘線程可以看見本身對該變量所作的修改

三、volatile修飾的變量不會被指令重排序優化,保證代碼的執行順序與程序順序相同

 

原子性、可見性、有序性

Java內存模型圍繞着併發過程當中如何處理原子性、可見性和有序性這三個特徵來創建的,來逐個看一下:

一、原子性(Atomicity)

由Java內存模型來直接保證原子性變量操做包括read、load、assign、use、store、write,大體能夠認爲基本數據類型的訪問讀寫是具有原子性的。若是應用場景須要一個更大的原子性保證,Java內存模型還提供了lock和unlock,儘管虛擬機沒有把lock和unlock操做直接開放給用戶使用,可是卻提供了更高層次的字節碼指令monitorenter和monitorexit來隱式地使用這兩個操做,這兩個字節碼指令反映到Java代碼中就是同步塊----synchronized關鍵字

二、可見性(Visibility)

可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。volatile其實已經詳細寫了這一點,其實synchronized關鍵字也是能夠實現可見性的,synchronized的可見性是由"對一個變量執行unlock操做以前,必須先把此變量同步回主內存中"這條規則得到的。另外,final關鍵字也能夠實現可見性,由於被final修飾的字段在構造器中一旦初始化完成,而且構造器沒有把this傳遞出去,那在其餘線程中就能看見final字段的值。

三、有序性(Ordering)

Java程序中自然的有序性能夠總結爲一句話:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另一個線程,全部的操做都是無須的。前半句是指"線程內表現爲穿行的語義",後半句是指"指令重排序"和"工做內存與主內存同步延遲"現象。Java語言提供了volatile和synchronized兩個關鍵字來保證線程之間操做的有序性,volatile關鍵字自己就包含了禁止指令重排序的語義,而synchronized則是由"一個變量在同一時刻只容許一條線程對其進行lock操做"這條規則得到的,這條規則規定了持有同一個鎖的兩個同步塊只能串行地進入

 

先行發生happens-before原則

若是Java內存模型中全部的有序性都僅僅靠volatile和synchronized來完成,那麼有一些操做將變得很繁瑣,可是咱們在編寫Java代碼時並未感受到這一點,這是由於Java語言中有一個"先行發生(happens-before)"原則。這個原則很是重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,咱們能夠經過幾條規則就判斷出併發環境下兩個操做之間是否可能存在衝突的問題。

所謂先行發生原則是指Java內存模型中定義的兩項操做之間的偏序關係,若是說操做A先行發生於操做B,那麼操做A產生的影響可以被操做b觀察到,"影響"包括修改了內存中共享變量的值、發送了消息、調用了方法等。Java內存模型下有一些自然的,不須要任何同步協助器就已經存在的先行發生關係:

一、程序次序規則:在一個線程內,按照控制流順序,控制流前面的操做先行發生於控制流後面的操做,說"控制流"是由於還要考慮到分支、循環結構

二、管程鎖定規則:一個unlock操做先行發生於後面對同一個鎖的lock操做

三、volatile變量規則:對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做

四、線程啓動規則:Thread對象的start()方法先行發生於此線程的每個動做

五、線程終止規則:線程中的全部操做都先行發生於對此線程的終止檢測

六、線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生

七、對象終結規則:一個對象的初始化完成先行發生於它的finalize()方法的開始

八、傳遞新:若是操做A先行發生於操做B,操做B先行發生於操做C,那麼操做A必然先行發生於操做C

Java語言無須任何同步手段保障就能成立的先行發生規則就只有上面這些額,若是兩個操之間的關係不在此列,而且沒法經過下面規則推導出來的話,它們就沒有順序性保障。舉一個例子來看一下:

複製代碼
private int i = 0;

public void setI(int i)
{
    this.i = i;
}

public int getI()
{
    return i;
}
複製代碼

很普通的一組getter/setter,假設A線程先調用了setI(1),B線程再調用了同一個對象的getI(),那麼B線程的返回值是什麼?

依次分析一下先行發生原則中的各項規則。因爲兩個方法分別由兩個線程分別調用,所以程序次序規則這裏不適用;因爲沒有同步塊,因此也就沒有unlock和lock,所以管程鎖定規則這裏不適用;i沒有被關鍵字volatile修飾,所以volatile變量規則這裏不適用;後面的啓動、終止、中斷、對象終結也和這裏徹底沒有關係,所以也都不適用。由於沒有一個實用的先行發生規則,因此最後一條傳遞性也無從談起,所以傳遞性也不適用。因爲全部先行發生原則都不適用,所以儘管線程A的setI(1)操做在時間上先發生,但沒法肯定線程B的getI()的返回結果,換句話說,這裏面的操做不是線程安全的。

那如何修復這個問題?至少有兩種比較簡單的辦法:

一、setter/getter都定義成synchronized的,這樣能夠套用管程鎖定規則

二、i定義爲volatile變量,因爲setter方法對i的修改不依賴於i的原值,知足volatile關鍵字的使用場景,這樣能夠套用volatile變量規則

轉載地址:http://www.cnblogs.com/xrq730/p/4859107.html

相關文章
相關標籤/搜索