一、Java內存模型的基礎
二、Java內存模型中的順序一致性
三、happens-before
四、同步原語(volatile、synchronized、final)
五、雙重檢查鎖定與延遲初始化
六、Java內存模型綜述
java
重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。程序員
若是兩個操做訪問同一個變量,且這兩個操做中有一個爲寫操做,此時這兩個操做之間就存在數據依賴性。數據依賴分下面這三種類型:編程
名稱 | 代碼示例 | 說明 |
---|---|---|
寫後讀 | a=1 b=a | 寫一個變量以後,再讀這個變量 |
寫後寫 | a=1 a=2 | 寫一個變量以後再次寫這個變量 |
讀後寫 | a=b b=1 | 讀一個變量以後再寫這個變量 |
對於這三種狀況,只要重排序兩個操做的執行順序,那麼程序的執行結果就會被改變。編譯器和處理器可能會對操做作重排序,編譯器和處理器在重排序時,會遵照數據依賴性,編譯器和處理器不會改變存在數據依賴關係的兩個操做的執行順序。(這裏說的數據依賴性僅針對單個處理器中執行的指令序列和單個線程中執行的操做,不一樣處理器之間和不一樣線程之間的數據依賴性不被編譯器和處理器考慮)安全
as-if-serial語義的意思是:無論怎麼重排序,程序的執行結果都不能被改變,編譯器、runtime和處理器都必須遵照as-if-serial語義。多線程
爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。可是,若是操做之間不存在數據依賴關係,這些操做就可能被編譯器和處理器重排序。示例以下:併發
double a = 3.14; // A
double b = 1.0; // B
dounle c = a * b * b; // C
複製代碼
如上述代碼,A和C之間存在依賴關係,B和C之間也存在依賴關係,所以在最終執行的指令序列內,C不能被重排序到A和B前,由於這樣程序的結果會被改變,可是A和B之間沒有依賴關係,編譯器和處理器能夠重排序A和B之間的執行順序:app
根據happens-before的程序順序規則,上面的代碼存在3個happens-before關係:編程語言
一、A happens-before B 二、B happens-before C 三、A happens-before Cpost
這裏的第3個happens-before關係,是根據happens-before的傳遞性推導出來的。性能
這裏A happens-before B,但實際執行時B卻能夠排在A以前執行,(由於他們之間沒有依賴關係),若是A happens-before B,Java內存模型並不要求A必定要在B以前執行,僅僅要求前一個操做對後一個操做可見,且前一個操做按順序排在第二個操做以前,這裏操做A的執行結果不須要對操做B可見;而且重排序操做A和操做B後的執行結果與不重排序的操做結果是一致的,這種狀況下,Java內存模型會認爲這種重排序不非法,Java內存模型容許這種重排序。
在計算機中,軟件計數和硬件計數有一個共同的目標;在不改變程序執行結果的前提下,儘量提升並行度。編譯器和處理器遵循這一目標,從happens-before的定義來看,Java內存模型一樣也遵照這一目標。
以下代碼:
class example{
int a = 0;
boolean flag = false;
public void writer(){
a = 1; // 1
flag = true; // 2
}
public void reader(){
if(flag){ // 3
int i = a * a; // 4
.....
}
}
}
複製代碼
假設有兩個線程A和B,A執行writer方法,B執行reader方法,B在執行到操做4的時候,可否看到A在操做1對共享變量a的寫入呢?
答案:不必定
緣由:操做1和操做2沒有依賴關係,編譯器和處理器能夠對1和2進行重排序,線程A先執行了2操做,而後線程B讀到的flag=true,就會進入,此時A尚未對a變量進行寫入,這裏多線程程序的語義就被重排序破壞了。
在單線程程序中,對存在控制依賴的操做重排序,不會改變執行結果(這也是as-if-serial語義容許對存在控制依賴的操做作重排序的緣由)。但在多線程程序中,對存在控制依賴的操做重排序,可能會改變程序的執行結果。
順序一致性內存模型是一個理論參考模型,在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型做爲參照。
當程序未正確同步時,就可能會存在數據競爭。Java內存模型規範對數據競爭的定義以下:
一、在一個線程中寫一個變量。
二、在另外一個線程中讀同一個變量。
三、寫和讀沒有經過同步來排序。
當代碼中包含數據競爭時,程序的執行每每產生違反直覺的結果(上面的示例就是這樣),若是一個多線程程序能正確同步,那麼這個程序將是一個沒有數據競爭的程序。
Java內存模型對正確同步的多線程程序的內存一致性作了以下保證:
若是程序是正確同步的,程序的執行將具備順序一致性--即程序的執行結果與該程序在順序一致性內存模型中的執行結果相同,這裏的同步是廣義上的同步,包括對經常使用同步原語(synchronized、volatile和final)的正確使用。
順序一致性內存模型是一個被計算機科學家理想化了的理論參考模型,它爲程序員提供了極強的內存可見性保證。順序一致性內存模型有兩大特徵:
一、一個線程中全部操做必須按照程序的順序來執行。
二、(無論程序是否同步)全部線程都只能看到一個單一的操做執行順序。在順序一致性內存模型中,每一個操做都必須原子執行且馬上對全部線程可見。
順序一致性內存模型視圖以下:
在概念上,順序一致性內存模型有一個單一的全局內存,這個內存經過一個左右擺動的開關能夠鏈接到任意一個線程。同時每個線程必須按照程序的順序來執行內存讀/寫操做。從上圖可知,在任意時間點最多隻有一個線程能夠鏈接到內存,當多個線程併發執行時,圖中的開關裝置能把全部線程的全部內存讀/寫操做串行化(即在順序一致性模型中,全部操做之間具備全序關係)
能夠看下面內容再理解:假設有兩個線程,A和B,兩個線程併發執行,其中A線程有三個操做,他們在程序中的執行順序爲:A1->A2->A3,一樣,B線程也有三個操做,他們在程序中的執行順序爲:B1->B2->B3。假設這兩個線程使用監視器鎖來正確同步,A線程三個操做以後釋放監視器鎖,隨後B線程獲取同一個監視器鎖,那麼程序在順序一致性內存模型中的執行順序以下:
若是兩個線程沒有作同步,下圖爲未同步程序在順序一致性內存模型中的執行示意圖:
未同步程序在順序一致性內存模型中雖然總體執行順序是無序的,但全部線程都只能看到一個一致的總體執行順序。如上圖,線程A和B看到的執行順序都是A1->B1->B2->A2->A3->B3。之因此能獲得這個保證是由於順序一致性內存模型中的每一個操做必須當即對任意線程可見。
可是在Java內存模型中沒有這個保證,未同步程序在Java內存模型中不但總體的執行順序是無序的,並且全部線程看到的操做執行順序也可能不一致。
這裏修改一下以前的程序,用鎖來作同步,看正確同步的程序如何保持順序一致性:
class example{
int a = 0;
boolean flag = false;
public synchronized void writer(){
a = 1;
flag = true;
}
public synchronized void reader(){
if(flag){
int i = a;
.....
}
}
}
複製代碼
上面示例中,假設A線程執行writer方法,B線程執行reader方法,這是一個正確同步的多線程程序。根據Java內存模型規範,該程序的執行結果將與該程序在順序一致性模型中的執行結果相同。下面是該程序在兩個內存模型中的執行時序對比圖:
在Java內存模型中,臨界區內的代碼能夠重排序(可是不容許臨界區的代碼「溢出」到臨界區以外,這樣會破壞監視器的語義)。Java內存模型會在退出臨界區和進入臨界區這兩個關鍵時間點作一些特別處理,使得線程在這兩個時間點具備有順序一致性內存模型的內存視圖。雖然線程A在臨界區內作了重排序,因爲監視器互斥執行的特性,這裏的線程B根本沒法觀察到線程A在臨界區內的重排序。這種重排序既提升了執行效率,又沒有改變程序的執行結果。
這裏能夠看到,Java內存模型的具體實現上的基本方針爲:在不改變(正確同步的)程序結果前提下,儘量的爲編譯器和處理器的優化打開了方便之門。
對於未同步或未正確同步的多線程程序,Java內存模型只提供最小安全性:線程執行時讀取到的值,要麼是以前某個線程寫入的值,要麼是默認值(0,null,false),Java內存模型保證線程讀操做讀取到的值不會是無中生有的。爲了實現最小安全性,JVM在堆上分配對象時,首先會對內存空間進行清零,而後纔會在上面分配對象(JVM內部會同步這兩個操做)。所以,在已清零的內存空間分配對象時,域的默認初始化已經完成了。
Java內存模型不保證未同步程序的執行結果與該程序在順序一致性內存模型中的執行結果相同(本該你作的事情,不能等到我來幫你作)。由於若是想要保證執行結果一致,Java內存模型要禁止大量的處理器和編譯器的優化,這對程序的執行性能會產生很大的影響。並且未同步程序在順序一致性模型中執行時,總體是無序的,其執行結果每每沒法預知。並且,保證未同步程序在這兩個模型中的執行結果一致沒有什麼意義。
未同步程序在兩個模型(順序一致性內存模型、Java內存模型)中的執行特性有以下幾個差別:
一、順序一致性模型保證單線程內的操做會按照程序的順序執行,而Java內存模型不保證單線程內的操做會按程序的順序執行(好比上面正確同步的多線程程序在臨界區內的重排序)。
二、順序一致性模型保證全部線程只能看到一致的操做執行順序,而Java內存模型不保證全部線程能看到一致的操做執行順序。
三、Java內存模型不保證對64位的long型和double型變量的寫操做具備原子性,而順序一致性模型保證對全部內存讀/寫操做都具備原子性。
在一些32位的處理器上,若是要求對64位數據的寫操做具備原子性,會有比較大的開銷。爲了照顧這些處理器,Java語言規範鼓勵但不強求JVM對64位的long類型變量和double類型變量的寫操做具備原子性。當JVM在這種處理器上運行時,可能會把一個64位long/double類型變量的寫操做拆分爲兩個32位的寫操做來執行。這兩個32位的寫操做可能會被分配到不一樣的總線事務中執行,此時對這個64位變量的寫操做不具備原子性。
當單個內存操做不具備原子性時,可能會產生意想不到的後果
如上圖所示,假設處理器A寫一個long型變量,同時處理器B要讀取這個long型變量。處理器A中64位的寫操做被拆分兩個32位的寫操做,且這兩個32位的寫操做分配到不一樣的事務中執行。同時處理器B中64位的讀操做被分配到單個的讀事務中執行。當處理器A和B按上圖來執行時,處理器B將看到僅僅被處理器A「寫了一半」的無效值,就至關於產生了髒讀數據。
在JSR-133規範以前的舊內存模型中,一個64位long/double型變量的讀/寫操做能夠被拆分兩個32位的讀/寫操做來執行。從JSR-133內存模型開始(即從JDK5開始),僅僅只容許一個64位long和double型變量的寫操做拆分爲兩個32位的寫操做來執行,任意的讀操做在JSR-133中都必須具備原子性(即任意讀操做必需要在單個事務中執行)。