1.對於final域,編譯器和處理器要遵照兩個重排序規則:java
(1)在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。程序員
(2)初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。編程
如下列代碼爲例進行解釋,假設線程A執行執行writer方法,線程B執行reader方法。數組
public class FinalExample { int i; // 普通變量 final int j; // final變量 static FinalExample obj; public FinalExample() { // 構造函數 i = 1; // 寫普通域 j = 2; // 寫final域 } public static void writer() { // 寫線程A執行 obj = new FinalExample(); } public static void reader() { // 讀線程B執行 FinalExample object = obj; // 讀對象引用 int a = object.i; // 讀普通域 int b = object.j; // 讀final域 } }
1. 寫final域的重排序規則禁止把final域的寫重排序到構造函數以外。這個規則的實現包含下面2個方面:安全
(1)JMM禁止編譯器把final域的寫重排序到構造函數以外。多線程
(2)編譯器會在final域的寫以後,構造函數return以前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數以外。併發
寫final域的重排序規則能夠確保:在對象引用爲任意線程可見以前,對象的final域已經被正確初始化過了,而普通域不具備這個保障。因此在上面的代碼中就可能會發生寫普通域的操做被編譯器重排序到了構造函數以外,讀線程B錯誤地讀取了普通變量i初始化以前的值。而寫final域的操做,被寫final域的重排序規則「限定」在了構造函數以內,讀線程B正確地讀取了final變量初始化以後的值。app
1. 讀final域的重排序規則是:在一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操做(注意,這個規則僅僅針對處理器)。編譯器會在讀final域操做的前面插入一個LoadLoad屏障。函數
初次讀對象引用與初次讀該對象包含的final域,這兩個操做之間存在間接依賴關係。因爲編譯器遵照間接依賴關係,所以編譯器不會重排序這兩個操做。大多數處理器也會遵照間接依賴,也不會重排序這兩個操做。但有少數處理器容許對存在間接依賴關係的操做作重排序 (好比alpha處理器),這個規則就是專門用來針對這種處理器的。性能
讀final域的重排序規則能夠確保:在讀一個對象的final域以前,必定會先讀包含這個final域的對象的引用,可是讀普通域就沒有這個保證。因此,在這個示例程序中,若是該引用不爲null,那麼引用對象的final域必定已經被A線程初始化過了,而若是讀普通域的指令被排在了讀obj對象引用以前,就會致使空指針異常,由於此時對於局部變量引用object尚未賦予對象。
如下列代碼爲例
public class FinalReferenceExample { final int[] intArray; // final是引用類型 static FinalReferenceExample obj; public FinalReferenceExample() { // 構造函數 intArray = new int[1]; // 1 intArray[0] = 1; // 2 } public static void writerOne() { // 寫線程A執行 obj = new FinalReferenceExample(); // 3 } public static void writerTwo() { // 寫線程B執行 obj.intArray[0] = 2; // 4 } public static void reader() { // 讀線程C執行 if (obj != null) { // 5 int temp1 = obj.intArray[0]; // 6 } } }
1.在上例代碼中,final域爲一個引用類型,它引用一個int型的數組對象。對於引用類型:
(1)寫final域的重排序規則對編譯器和處理器增長了以下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。也就是說,1是對final域的寫入,2是對這個final域引用的對象的成員域的寫入,3是把被 構造的對象的引用賦值給某個引用變量。這裏除了前面提到的1不能和3重排序外,2和3也不能重排序。
(2)假設線程A先執行,執行完成後再執行線程B和C,那麼,JMM能夠確保讀線程C至少能看到寫線程A在構造函數中對final引用對象的成員域的寫入。即C至少能看到數組下標0的值爲1。而寫線程B對數組元素的寫入,讀線程C可能看獲得, 也可能看不到。JMM不保證線程B的寫入對讀線程C可見,由於寫線程B和讀線程C之間存在數據競爭,此時的執行結果不可預知。 若是想要確保讀線程C看到寫線程B對數組元素的寫入,寫線程B和讀線程C之間須要使用同步原語(lock或volatile)來確保內存可見性。
寫final域的重排序規則能夠確保:在引用變量爲任意線程可見以前,該引用變量指向的對象的final域已經在構造函數中被正確初始化過了。其實,要獲得這個效果, 還須要一個保證:在構造函數內部,不能讓這個被構造對象的引用爲其餘線程所見,也就是對象引用不能在構造函數中「逸出」。若是沒有這個保證會發生什麼?以下面代碼所示
public class FinalReferenceEscapeExample { final int i; static FinalReferenceEscapeExample obj; public FinalReferenceEscapeExample() { i = 1; // 1寫final域 obj = this; // 2 this引用在此"逸出" } public static void writer() { new FinalReferenceEscapeExample(); } public static void reader() { if (obj != null) { // 3 int temp = obj.i; // 4 } } }
假設一個線程A執行writer()方法,另外一個線程B執行reader()方法。這裏的操做2使得對象還未完成構造前就爲線程B可見。即便這裏的操做2是構造函數的最後一步,且在程序中操做2排在操做1後面,執行read()方法的線程仍然可能沒法看到final域被初始化後的值,由於這裏的操做1和操做2之間可能被重排序。
因此,在構造函數返回前,被構造對象的引用不能爲其餘線程所見,由於此時的final域可能尚未被初始化。在構造函數返回後,任意線程都將保證能看到final域正確初始化以後的值。
寫final域的重排序規則會要求編譯器在final域的寫以後,構造函數return以前插入一個StoreStore屏障。讀final域的重排序規則要求編譯器在讀final域的操做前面插入 一個LoadLoad屏障。
可是某些處理器,以X86處理器爲例,它不支持寫-寫重排序,因此寫final域操做後插入的StoreStore屏障會被省略。其次X86處理器不會對存在間接關係依賴的數據操做進行重排序,因此讀final域前的LoadLoad屏障也會被省略。也就是說X86處理器中,final的讀寫不須要插入任何內存屏障。
1.在設計JMM時,須要考慮兩個關鍵因素:
(1)程序員對內存模型的使用。程序員但願內存模型易於理解、易於編程。程序員但願基於一個強內存模型來編寫代碼。
(2)編譯器和處理器對內存模型的實現。編譯器和處理器但願內存模型對它們的束縛越少越好,這樣它們就能夠作儘量多的優化來提升性能。編譯器和處理器但願實現一個弱內存模型。
因此,設計JMM時,必定要在以上兩個因素之間找到一個平衡點,一方面,要爲程序員提供足夠強的內存可見性保證;另外一方面,對編譯器和處理 器的限制要儘量地放鬆。
2. 如下列代碼爲例:
double pi = 3.14; // A double r = 1.0; // B double area = pi * r * r; // C
上面計算圓的面積的示例代碼存在3個happens-before關係:
(1)A happens-before B。
(2)B happens-before C。
(3)A happens-before C。
在3個happens-before關係中,2和3是必需的,但1是沒必要要的。所以,JMM把happens-before 要求禁止的重排序分爲了下面兩類:
(1)會改變程序執行結果的重排序。
(2)不會改變程序執行結果的重排序。
JMM對這兩種不一樣性質的重排序,採起了不一樣的策略:
(1)對於會改變程序執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。
(2)對於不會改變程序執行結果的重排序,JMM對編譯器和處理器不作要求(JMM容許這種重排序)。
也就是說
(1)JMM向程序員提供的happens-before規則能知足程序員的需求。JMM的happens-before規則不但簡單易懂,並且也向程序員提供了足夠強的內存可見性保證(有些內存可見性保證其實並不必定真實存在,好比上面的A happens-before B)。
(2)JMM對編譯器和處理器的束縛已經儘量少。從上面的分析能夠看出,JMM實際上是在遵循一個基本原則:只要不改變程序的執行結果(指的是單線程程序和正確同步的多線程程序), 編譯器和處理器怎麼優化都行。例如,若是編譯器通過細緻的分析後,認定一個鎖只會被單個線程訪問,那麼這個鎖能夠被消除。再如,若是編譯器通過細緻的分析後,認定一個volatile變 量只會被單個線程訪問,那麼編譯器能夠把這個volatile變量看成一個普通變量來對待。
1.定義以下:
(1)若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。
(2)兩個操做之間存在happens-before關係,並不意味着Java平臺的具體實現必需要按照 happens-before關係指定的順序來執行。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM容許這種重排序)。
對於程序員來講,定義(1)的關係表示:若是A happens-before B,那麼Java內存模型將向程序員保證——A操做的結果將對B可見, 且A的執行順序排在B以前。
定義(2)是JMM對編譯器和處理器重排序的約束原則。正如前面所言,JMM實際上是在遵 循一個基本原則:只要不改變程序的執行結果(指的是單線程程序和正確同步的多線程程序), 編譯器和處理器怎麼優化都行。JMM這麼作的緣由是:程序員對於這兩個操做是否真的被重 排序並不關心,程序員關心的是程序執行時的語義不能被改變(即執行結果不能被改變)。因 此,happens-before關係本質上和as-if-serial語義是一回事。
2. as-if-serial與happens-before對比:
(1)as-if-serial語義保證單線程內程序的執行結果不被改變,happens-before關係保證正確同步的多線程程序的執行結果不被改變。
(2)as-if-serial語義給編寫單線程程序的程序員一個假象,即單線程程序是按程序的順序來執行的。happens-before關係給編寫正確同步的多線程程序的程序員一個假象,即正確同步的多線程程序是按happens-before指定的順序來執行的。
1. happens-before是JMM最核心的概念。其四個規則分別是
(1)程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做。
(2)監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
(3)volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
(4)傳遞性:若是A happens-before B,且B happens-before C,那麼A happens-before C。
(5)start()規則:若是線程A執行操做ThreadB.start()(啓動線程B),那麼A線程的ThreadB.start()操做happens-before於線程B中的任意操做。
(6)join()規則:若是線程A執行操做ThreadB.join()併成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。
2. 以volatile寫-讀創建的happens-before關係爲例:
首先,在volatile寫操做前會插入一個StoreStore屏障,保證在volatile變量寫操做以前的其餘寫操做必定會先被執行,也就是:操做1happens-before 操做2。
而在volatile變量寫操做以後,會插入一個StoreLoad屏障,其保證了在volatile變量寫操做以後的全部讀寫操做必定會在volatile變量寫操做執行完畢以後在執行,也就是:操做2 happens-before 操做3
在每一個volatile讀操做的後面插入一個LoadLoad屏障,保證了讀操做必定會在其後的讀操做以前執行,也就是 操做3 happens-before 操做4。
根據happens-before規則的傳遞性,能夠得出,操做1 happens-before 操做4。也就是操做1必定對操做4可見。
3.以線程方法 Thread.start() 創建的happens-before關係爲例:假設線程A在執行的過程當中,經過執行ThreadB.start()來啓動線程B。同時,假設線程A在執行ThreadB.start()以前修改了一些共享變量,線程B在開始執行後會讀這些共享變量。
1 happens-before 2由程序順序規則產生。2 happens-before 4由start()規則產生。根據傳遞性,將有1 happens-before 4。這實意味着,線程A在執行ThreadB.start()以前對共享變量所作的修改,接下來在線程B開始執行後都將確保對線程B可見。
在Java多線程程序中,有時候須要採用延遲初始化來下降初始化類和建立對象的開銷。雙重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法。分析雙重檢查鎖定的錯誤根源,以及兩種線程安全的延遲初始化方案。
1. 在Java程序中,有時候可能須要推遲一些高開銷的對象初始化操做,而且只有在使用這些對象時才進行初始化。此時,程序員可能會採用延遲初始化。但要正確實現線程安全的延遲初始化須要一些技巧,不然很容易出現問題。好比,下面是非線程安全的延遲初始化對象的示例代碼。
public class UnsafeLazyInitialization { private static Instance instance; public static Instance getInstance() { if (instance == null) // 1:A線程執行 instance = new Instance(); // 2:B線程執行 return instance; } }
在UnsafeLazyInitialization類中,假設A線程執行代碼1的同時,B線程執行代碼2。此時,線程A可能會看到instance引用的對象尚未完成初始化。咱們能夠對getInstance()方法作同步處理來實現線程安全 的延遲初始化。示例代碼以下。
public class SafeLazyInitialization { private static Instance instance; public synchronized static Instance getInstance() { if (instance == null) instance = new Instance(); return instance; } }
因爲對getInstance()方法作了同步處理,synchronized將致使性能開銷。若是getInstance()方法被多個線程頻繁的調用,將會致使程序執行性能的降低。反之,若是getInstance()方法不會被多個線程頻繁的調用,那麼這個延遲初始化方案將能提供使人滿意的性能。
爲了不鎖帶來的巨大開銷,雙重檢查鎖定便出現了(Double-Checked Locking),經過雙重檢查鎖定來下降同步的開銷。下面是使用雙重檢查鎖定來實現延遲初始化的示例代碼。
public class DoubleCheckedLocking { // 1 private static Instance instance; // 2 public static Instance getInstance() { // 3 if (instance == null) { // 4:第一次檢查 synchronized (DoubleCheckedLocking.class) { // 5:加鎖 if (instance == null) // 6:第二次檢查 instance = new Instance(); // 7:問題的根源出在這裏 } // 8 } // 9 return instance; // 10 } // 11 }
若是第一次檢查instance不爲null,那麼就不須要執行下面的加鎖和初始化操做。所以,能夠大幅下降synchronized帶來的性能開銷。可是,問題會出如今第7行代碼處。若一個在另外一個線程執行到第4行,代碼讀取到instance不爲null時,instance引用的對象有可能尚未完成初始化。
對於instance = new Instance()的這行代碼的過程爲:
(1)分配內存空間
(2)初始化對象
(3)將instance的引用指向開闢的內存地址
可是,對於2,3步,編譯器可能會進行重排序,也就是說會先將instance的引用指向剛開闢的內存地址,這個行爲就意味着instance = new Instance()這行代碼執行完畢,接下來會釋放鎖(這也就會致使其餘線程此時就有可能開始執行,判斷instance引用不爲空,此時,其餘線程就會看到一個未被初始化的對象),而後再初始化對象。
對於2,3步驟的重排序,只要在初次訪問對象以前執行完兩個步驟,單線程內的執行結果並不會改變,可是多線程時,就有可能形成其餘線程看到的是一個未被初始化的對象。
所以,有兩個辦法來實現線程安全的延遲初始化:
(1)不容許2和3重排序。
(2)容許2和3重排序,但不容許其餘線程「看到」這個重排序。
如下的兩個解決方法依據這兩個原理。
1. 對於前面的基於雙重檢查鎖定來實現延遲初始化的方案(指DoubleCheckedLocking示例代 碼),只須要作一點小的修改(把instance聲明爲volatile型),就能夠實現線程安全的延遲初始化。請看下面的示例代碼:
public class SafeDoubleCheckedLocking { private volatile static Instance instance; public static Instance getInstance() { if (instance == null) { synchronized (SafeDoubleCheckedLocking.class) { if (instance == null) instance = new Instance(); // instance爲volatile,如今沒問題了 } } return instance; } }
這個解決方案須要JDK 5或更高版本(由於從JDK 5開始使用新的JSR-133內存模型規範,這個規範加強了volatile的語義)。
當聲明對象的引用爲volatile後,instance = new Instance()這行代碼的三個步驟中的2和3之間的重排序,在多線程環境中將會被禁止。
1. JVM在類的初始化階段(即在Class被加載後,且被線程使用以前),會執行類的初始化。在 執行類的初始化期間,JVM會去獲取一個鎖。這個鎖能夠同步多個線程對同一個類的初始化。
public class InstanceFactory { private static class InstanceHolder { public static Instance instance = new Instance(); } public static Instance getInstance() { return InstanceHolder.instance ; // 這裏將致使InstanceHolder類被初始化 } }
假設兩個線程併發執行getInstance()方法,下面是執行的示意圖
這個方案的實質是:容許2和3重排序,但不容許非構造線程(這裏指線程B)「看到」這個重排序。
2. 初始化一個類,包括執行這個類的靜態初始化和初始化在這個類中聲明的靜態字段。根據Java語言規範,在首次發生下列任意一種狀況時,一個類或接口類型T將被當即初始化。
(1)T是一個類,並且一個T類型的實例被建立。
(2)T是一個類,且T中聲明的一個靜態方法被調用。
(3)T中聲明的一個靜態字段被賦值。
(4)T中聲明的一個靜態字段被使用,並且這個字段不是一個常量字段。
(5)T是一個頂級類,並且一個斷言語句嵌套在T內部被執行。 在InstanceFactory示例代碼中,首次執行getInstance()方法的線程將致使InstanceHolder類被初始化(符合狀況4)。
3. 對於類或接口的初始化,Java語言規範制定了精巧而複雜的類初始化處理過程。Java初始化一個類或接口的處理過程以下:
(1)經過在Class對象上同步(即獲取Class對象的初始化鎖),來控制類或接口的初始化。這個獲取鎖的線程會一直等待,直到當前線程可以獲取到這個初始化鎖。假設Class對象當前尚未被初始化(初始化狀態state,此時被標記爲state=noInitialization),且有兩個線程A和B試圖同時初始化這個Class對象。如圖
(2)線程A執行類的初始化,同時線程B在初始化鎖對應的condition上等待。線程A執行初始化後,線程B檢查到state已經初始化後,則釋放初始化鎖。線程A執行初始化的過程當中就包括對靜態字段的初始化,以及對靜態內部類中靜態字段的初始化,在對靜態字段的對象初始化步驟中的2,3步步驟能夠被重排序,可是沒法被其餘線程看到。
(3)線程A設置state=initialized,而後喚醒在condition中等待的全部線程。
(4)線程B結束類的初始化處理。
(5)線程C執行類的初始化的處理。
在第3階段以後,類已經完成了初始化。所以線程C在第5階段的類初始化處理過程相對簡單一些(前面的線程A和B的類初始化處理過程都經歷了兩次鎖獲取-鎖釋放,而線程C的類初 始化處理只須要經歷一次鎖獲取-鎖釋放)。
若是確實須要對實例字段使用線程安全的延遲初始化,請使用上面介紹的基於volatile的延遲初始化的方案;若是確實須要對靜態字段使用線程安全的延遲初始化,請使用上面介紹的基於類初始化的方案。