java內存模型

java內存模型

java內存模型基礎

happen-before模型

JSR-133使用happen-before的概念來闡述操做之間的內存可見性。在JMM中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在happen-before關係。在這裏兩個操做能夠在一個線程以內,也能夠在不一樣的線程之間。與程序員相關的happen-before規則以下:java

  1. 程序順序一致性:一個線程中的每一個操做,happen-before於該線程中的任意後續操做。(不要扣字,若兩操做沒有依賴關係,且變動操做順序不影響結果,此時順序能夠變動。與程序一致性規則不衝突)
  2. 監視器鎖規則: 對一個鎖的解鎖,happen-before於隨後對這個鎖的加鎖。
  3. volatile變量規則:對一個volatile域的寫操做,happen-before於任意後續對這個volatile域的讀。
  4. 傳遞性:若是A happen-before B且B happen-before C ,那麼A happen-before C。
  5. start()規則:若是線程A執行操做ThreadB.start(),那麼A線程的ThreadB.start()操做happen-before於B中的任何操做。
  6. join()規則:若是ThreadA 執行操做ThreadB.join()併成功返回,那麼ThreadB中的任意操做happen-before於ThreadA從ThreadB.join()操做成功返回。

兩個操做間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行。happens-before僅僅要求前一個操做對後一個操做可見。程序員


重排序

重排序是指編譯器和處理器爲了優化程序性能而對指令序列進行從新排序的一種手段。重排序得遵循如下原則。編程

  • 數據相互信賴的兩個操做不能進行重排序
  • as-if-serial語言,無論怎麼得排序(編譯器和處理器爲了提升並行度),單線程程序執行的結果不能改變。
  • 重排序對多線程的影響,代碼以下:
/**
 * 操做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
        }
    }
}

順序一致性

順序一致性內存模型是一個理論參考模型,在設計的時候,處理器的內存模型和編程語言的內存模型都會以順序一致性內存模型做爲參照。順序一致性內存模型有兩大特性:數組

  1. 一個線程中全部操做必須按照程序的順序執行。
  2. (無論程序是否同步)全部線程都只能看到單一的操做執行順序,在順序一致性內存模型中,每一個操做都必須原子執行且當即對全部線程可見。

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

  1. 順序一致性模型保證單線程內的操做會按程序的順序執行,而JMM不保證單線程內的操做會按程序的順序執行(好比上面正確同步的多線程程序在臨界區內的重排序).
  2. 順序一致性模型保證全部線程只能看到一致的操做執行順序,而JMM不保證全部線程能看到一致的操做執行順序。
  3. JMM不保證對64位的long型和double型變量的寫操做具備原子性,而順序一致性模型保證對全部的內存讀/寫操做都具備原子性。

volatile內存語義

volatile特性

  • 可見性:對一個volatile變量的讀,老是能看到(任意線程)對這個volatile變量最後的寫入。
  • 原子性: 對任意單個volatile變量的讀/寫具備原子性,但相似於volatile++這種複合操做不具備原子性。
class VolatileFeaturesExample {
    volatile long vl = 0L;
    public void set(long l) {
        vl = l;
    }
    public void getAndIncrement () {   //複合volatile讀寫,不具備線程安全
        vl++;
    }
    public long get() {
        return vl;
    }
}

volatile 寫-讀創建的happen-before關係

示例代碼以下:編程語言

  1. 根據程序次序規則,1 happen-before 2, 3 happen-before 4.(疑問1:1與2,3與4兩操做沒有依賴,爲什麼不能重排,若發生重排,結果有可能會發生變化)
  2. 根據volatile規則 2 happen-before 3
  3. 根據happen-before傳遞性規則,1 happen-before 4.
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 寫-讀內存原語

  • volatile寫操做,JMM會把線程對應的本地內存中的共享變量值刷新到主內存。
  • volatile讀操做,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。
volatile 內存語義的實現

爲了實現volatile內存語義,JMM分分別限制這兩種重排序類型,下圖JMM針對編譯器制定的volatile重排序規則表函數

圖片描述

這個重排序規則解釋了疑問1。實現:是經過編譯器生成字節碼時,插入內存屏障來達到這個限制,在此處不做展開,有興趣能夠查閱相關資料性能

JSR-133加強volatile的內存原語

在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類。

  1. 根據程序次序規則,1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6。
  2. 根據監視器鎖規則,3 happens-before 4。
  3. 根據happens-before的傳遞性,2 happens-before 5。

鎖的釋放與獲取的內存語義

  • 線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
  • 線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量

final 域內存語義

final 域,編譯器與處理器要遵照兩個重排序規則

  • 在構造函數內對一個final域的寫入,與隨後把這個被構造對像的引用賦值給一個引用變量,這兩個操做之間不能重排序
  • 初次讀一個包含final域的對象引用,與隨後初次讀這個final域,這兩個操做之間不能重排序(有點擾,eg:obj,obj.j的關係)

下面的示例代碼,說明這兩個規則

/**
 * 
 */
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;
    }
}

寫final域重排序規則

  • MM禁止編譯器把final域的寫重排序到構造函數以外。
  • JMM編譯器會在final域的寫以後,構造函數return以前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數以外

讀final域重排序規則

  • 一個線程中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操做

分析上面代碼示例 reader()方法包含3個操做。

  • 初次讀引用變量obj。
  • 初次讀引用變量obj指向對象的普通域j。
  • 初次讀引用變量obj指向對象的final域

final域爲引用類型

/**
 *假設首先線程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引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。

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
    }
}

結論:在構造函數返回前,被構造對象的引用不能爲其餘線程所見

雙重檢查鎖定與延遲初始化

java內存模型綜述

相關文章
相關標籤/搜索