JSR-133使用happen-before的概念來闡述操做之間的內存可見性。在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在happen-before關係。在這裏兩個操做能夠在一個線程以內,也能夠在不一樣的線程之間。與程序員相關的happen-before規則以下:java
兩個操做間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行。happens-before僅僅要求前一個操做對後一個操做可見。程序員
重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。重排序得遵循如下原則。編程
/** * 操做1 操做2 之間無依賴關係, 能夠進行重排序 * 操做3 操做4 之間無依賴關係, 能夠進行重排序 * Thread B 中並不必定能看到Thread A 中對共享變量的寫入。此時重排序操做破壞多線程語義 **/ class ReorderExample{ int a = 0; boolean flag = false; public void writer(){ //Thread A a = 1; //1 flag = true; //2 } public void reader(){ //Thread B if(flag){ //3 int i = a * a; //4 } } }
順序一致性內存模型是一個理論參考模型,在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型做爲參照。順序一致性內存模型有兩大特性:數組
JMM對正確同步的多線程程序的內存一致性作了以下保證
若是程序是正確同步的,程序的執行將具備順序一致性(Sequentially Consistent)--即程序的執行結果與該程序在順序一致性內存模型中執行結果相同。這裏的同步包括對經常使用同步原語(Synchronized,volatile,final)的正確使用 安全
經過如下程序說明JMM與順序一致性 兩種內存模型的對比多線程
/** *順序一致性模型中,全部操做徹底按程序的順序串行執行。而在JMM中,臨界區內的代碼 *能夠重排序(但JMM不容許臨界區內的代碼「逸出」到臨界區以外,那樣會破壞監視器的語 *義)。JMM會在退出臨界區和進入臨界區這兩個關鍵時間點作一些特別處理,使得線程在這兩 *個時間點具備與順序一致性模型相同的內存視圖,雖然線程A在臨界 *區內作了重排序,但因爲監視器互斥執行的特性,這裏的線程B根本沒法「觀察」到線程A在臨 *界區內的重排序。這種重排序既提升了執行效率,又沒有改變程序的執行結果。 * */ class SynchronizedExample { int a = 0; boolean flag = false; public synchronized void writer() { // 獲取鎖 a = 1; flag = true; } // 釋放鎖 public synchronized void reader() { // 獲取鎖 if (flag) { int i = a; ...... } // 釋放鎖 }
未同步程序在JMM中的執行時,總體上是無序的,其執行結果沒法預知。app
class VolatileFeaturesExample { volatile long vl = 0L; public void set(long l) { vl = l; } public void getAndIncrement () { //複合volatile讀寫,不具備線程安全 vl++; } public long get() { return vl; } }
示例代碼以下:編程語言
class VolatileExample { int a = 0; volatile boolean flag = false; public void writer() { a = 1; // 1 flag = true; // 2 } public void reader() { if (flag) { // 3 int i = a; // 4 ...... } }
爲了實現volatile內存語義,JMM分分別限制這兩種重排序類型,下圖JMM針對編譯器制定的volatile重排序規則表函數
這個重排序規則解釋了疑問1。實現:是經過編譯器生成字節碼時,插入內存屏障來達到這個限制,在此處不做展開,有興趣能夠查閱相關資料性能
在JSR-133以前的舊Java內存模型中,雖然不容許volatile變量之間重排序,但舊的Java內存模型容許volatile變量與普通變量重排序
所以,在舊的內存模型中,volatile的寫-讀沒有鎖的釋放-獲所具備的內存語義。爲了提供一種比鎖更輕量級的線程之間通訊的機制,JSR-133專家組決定加強volatile的內存語義:嚴格限制編譯器和處理器對volatile變量與普通變量的重排序,確保volatile的寫-讀和鎖的釋放-獲取具備相同的內存語義。從編譯器重排序規則和處理器內存屏障插入策略來看,只要volatile變量與普通變量之間的重排序可能會破壞volatile的內存語義,這種重排序就會被編譯器重排序規則和處理器內存屏障插入策略禁止。
當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。
當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。
/** * */ class MonitorExample { int a = 0; public synchronized void writer() { // 1 a++; // 2 } // 3 public synchronized void reader() { // 4 int i = a; // 5 ...... } // 6 }
假設線程A執行writer()方法,隨後線程B執行reader()方法。根據happens-before規則,這個過程包含的happens-before關係能夠分爲3類。
final 域,編譯器與處理器要遵照兩個重排序規則
下面的示例代碼,說明這兩個規則
/** * */ public class FinalExample{ int i; final int j; static FinalExample obj; static FinalExample(){ i = 1; j = 2; } public static void writer(){ obj = new FinalExample(); } public static void reader(){ FinalExample object = obj; int a = obj.i; int b = obj.j; } }
分析上面代碼示例 reader()方法包含3個操做。
/** *假設首先線程A執行writeOne方法,執行完後線程B執行writetwo()方法,執行完後線程執行reader()方法 *1是對final域的寫入,2是對這個final域引用的對象的成員域的寫入,3是把被構造的對象的引用賦值給某個引用變量。這裏除了前面提到的1不能和3重排序外,2和3也不能重排序 */ public class FinalReferenceExample { final int[] intArray; 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 } }
本例final域爲一個引用類型,它引用一個int型的數組對象。對於引用類型,寫final域的重排序規則對編譯器和處理器增長了以下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。
/** * 步驟2使得構造函數還未完成就對reader線程可見 **/ 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 } }
結論:在構造函數返回前,被構造對象的引用不能爲其餘線程所見