1. 程式人生 > >Java內存模型

Java內存模型

應用 vol -h 詳細 cit line blog 引擎 自然

一、什麽是Java內存模型

Java虛擬機規範中試圖定義一種Java內存模型(Java Memory Model,JMM)來屏蔽掉各種硬件和操作系統的訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。在此之前,主流程序語言(如C/C++等)直接使用物理硬件和操作系統的內存模型,因此,會由於不同平臺上內存模型的差異,有可能導致程序在一套平臺上並發完全正常,而在另外一套平臺上並發訪問卻經常出錯,因此在某些場景下就不許針對不同的平臺來編寫程序。

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

二、主內存和工作內存

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

Java內存模型中規定了所有的變量都存儲在主內存中(如虛擬機物理內存中的一部分),每條線程還有自己的工作內存(如CPU中的高速緩存),線程的工作內存中保存了該線程使用到的變量到主內存的副本拷貝,線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量

。不同線程之間無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成,線程、主內存和工作內存的交互關系如下圖所示:

技術分享

三、內存間交互操作

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

1、lock(鎖定):作用於主內存中的變量,它把一個變量標識為一條線程獨占的狀態。

2、unlock(解鎖):作用於主內存中的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。

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

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

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

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

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

8、write(寫入):作用於主內存中的變量,它把store操作從工作內存中得到的變量值放入主內存的變量中。

如果要把一個變量從主內存中復制到工作內存,就需要按順尋地執行以下兩個操作:

(1)由JVM主內存執行的讀(read)操作;

(2)由Java線程的工作內存執行相應的load操作。

反過來,如果把變量從工作內存中同步回主內存中,也出現兩個操作:

(1)由Java線程的工作內存執行的存儲(store)操作;

(2)由JVM主內存執行的相應的寫(write)操作。

Java內存模型只要求上述操作必須按順序執行,而沒有保證必須是連續執行。也就是read和load之間,store和write之間是可以插入其他指令的,如對主內存中的變量a、b進行訪問時,可能的順序是read a,read b,load b, load a。

Java內存模型還規定了在執行上述八種基本操作時,必須滿足如下規則:

1、不允許read和load、store和write操作之一單獨出現。

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

3、不允許一個線程無原因地把數據從線程的工作內存同步回主內存中。

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

5、一個變量在同一時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock後,只有執行相同次數的unlock操作,變量才會被解鎖。

6、如果對同一個變量執行lock操作,那將會清空工作內存中此變量的值,在執行引擎使用這個變量前,需要重新執行load或assign操作初始化變量的值。

7、如果一個變量事先沒有被lock操作鎖定,那就不允許對它進行unlock操作,也不允許去unlock一個被其他線程鎖定的變量。

8、對一個變量執行unlock操作之前,必須先把此變量同步回主內存中。

四、volatile型變量的特殊規則

上面說過,read,load,store,write的操作都是原子的,即執行期間不會被中斷!但是各個原子操作之間可能會發生中斷對於普通變量,如果一個線程中那份主內存變量值的拷貝更新了,並不能馬上反應在其他變量中,因為Java的每個線程都私有一個工作內存,裏面存儲了該條線程需要用到的主內存中的變量拷貝!(比如實例的字段信息,類型的靜態變量,數組,對象……)如圖:

技術分享

A,B兩條線程直接讀or寫的都是線程的工作內存!而A、B使用的數據從各自的工作內存傳遞到同一塊主內存的這個過程是有時差的,或者說是有隔離的!通俗的說他們之間看不見!也就是之前說的一個線程中的變量被修改了,是無法立即讓其他線程看見的!如果需要在其他線程中立即可見需要使用 volatile 關鍵字。現在引出volatile關鍵字:

關鍵字volatile可以說是Java虛擬機提供的最輕量級的同步機制。一個變量被定義為volatile後,它將具備兩種特性:

1、保證此變量對所有線程的"可見性"所謂"可見性"是指當一條線程修改了這個變量的值,新值對於其它線程來說都是可以立即得知的,而普通變量不能做到這一點,普通變量的值在在線程間傳遞均需要通過主內存來完成。例如,線程A修改一個普通變量的值,然後將變量的值寫回主內存,另外一個線程B在線程A回寫完成了之後再從主內存進行讀取操作,新變量值才會對線程B可見。另外,java裏面的運算並非原子操作,會導致volatile變量的運算在並發下一樣是不安全的。再強調一遍,volatile只保證了可見性,並不保證基於volatile變量的運算在並發下是安全的。

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

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

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

2、在工作內存中,每次修改完某個變量後都必須立刻同步回主內存中,用於保證其他線程能夠看見自己對該變量所做的修改。

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

五、原子性、可見性、有序性

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

1、原子性(Atomicity)

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

什麽是原子性?

在Java中,對基本數據類型的變量的操作是原子性操作,即這些操作是不可被中斷的,要麽執行,要麽不執行。看例子:

int x = 10;         //語句1
y = x;               //語句2
x++;                //語句3
x = x + 1;        //語句4

這幾個語句哪幾個是原子操作?

其實只有語句1是原子性操作,其他三個語句都不是原子性操作。語句1是直接將數值10賦值給x,也就是說線程執行這個語句會直接將數值10寫入到工作內存中。線程執行語句2實際上包含2個操作,它先要去主內存讀取x的值,再將x的值寫入工作內存,雖然讀取x的值以及將x的值寫入工作內存這2個操作都是原子性操作,但是合起來就不是原子性操作了。同樣的,x++和 x = x+1包括3個操作:讀取x的值,進行加1操作,寫入新的值。所以上面4個語句只有語句1的操作具備原子性。也就是說,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。

2、可見性(Visibility)

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

什麽是可見性?

大白話就是一個線程修改了變量,其他線程可以立即能夠知道。保證可見性可以使用之前提到的volatile關鍵字(強制立即寫入主內存,使得其他線程共享變量緩存行失效),還有重量級鎖synchronized (也就是線程間的同步,unlock之前,寫變量值回主存,看作順序執行的),最後就是常量——final修飾的(一旦初始化完成,其他線程就可見)。其實這裏忍不住還是補充下,關鍵字volatile 的語義除了保證不同線程對共享變量操作的可見性,還能禁止進行指令重排序!也就是保證有序性。

3、有序性(Ordering)

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

什麽是有序性和重排序?

還是大白話,在本線程內,所有的操作看起來都是有序的,但是在本線程之外(其他線程)觀察,這些操作都是無序的。涉及到了:

  • 指令重排(破壞線程間的有序性)
  • 之前說的工作內存和主內存同步延時(也就是線程A先後更新兩個變量m和n,但是由於線程工作內存和JVM主內存之間的同步延時,線程B可能還沒完全同步線程A更新的兩個變量,可能先看到了n……對於B來說,它看A的操作就是無序的,順序無法保證)。

六、先行發生happens-before原則

如果Java內存模型中所有的有序性都僅僅靠volatile和synchronized來完成,那麽有一些操作將變得很繁瑣,但是我們在編寫Java代碼時並未感覺到這一點,這是因為Java語言中有一個"先行發生(happens-before)"原則。這個原則非常重要,它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,我們可以通過幾條規則就判斷出並發環境下兩個操作之間是否可能存在沖突的問題。

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

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

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

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

4、線程啟動規則:Thread對象的start()方法先行發生於此線程的每一個動作。

5、線程終止規則:線程中的所有操作都先行發生於對此線程的終止檢測。

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

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

8、傳遞新:如果操作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()的返回結果,換句話說,這裏面的操作不是線程安全的。

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

1、setter/getter都定義成synchronized的,這樣可以套用管程鎖定規則

2、i定義為volatile變量,由於setter方法對i的修改不依賴於i的原值,滿足volatile關鍵字的使用場景,這樣可以套用volatile變量規則

Java內存模型