Java併發程式設計:Java記憶體模型

KiteRunner24發表於2018-03-19

一、Java記憶體模型基礎

1. 兩個關鍵問題

  • 執行緒之間如何通訊;
  • 執行緒之間如何同步。

執行緒之間的通訊機制:共享記憶體+訊息傳遞

在共享記憶體的併發模型裡,執行緒之間共享程式的公共狀態,通過讀寫記憶體中的公共狀態進行隱式通訊。
在訊息傳遞的併發模型裡,執行緒之間沒有公共狀態,執行緒之間必須通過傳送訊息來顯式進行通訊。

Java併發採用的是共享記憶體模型,Java執行緒之間的通訊總是隱式進行的。

2. Java記憶體模型的抽象結構

Java執行緒之間的通訊由Java記憶體模型控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。

JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體中儲存了該執行緒以讀寫共享變數的副本。

Java併發程式設計:Java記憶體模型

從上圖可以看出,如果執行緒A和執行緒B之間要通訊的話,必須要經歷兩個步驟:

  1. 執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去;
  2. 執行緒B到主記憶體中去讀取執行緒A之前已經更新過的共享變數。

Java併發程式設計:Java記憶體模型

從整體上看,上述兩個步驟實質上是執行緒A在向執行緒B傳送訊息,而且這個通訊過程必須要經過主記憶體。JMM通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為Java程式設計師提供記憶體可見性保證。

3. 指令序列的重排序

為了提高效能,編譯器和處理器常常會對指令進行重排序。

重排序包括以下三種型別:

  1. 編譯器優化的重排序
  2. 指令級並行的重排序——指令級並行技術。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
  3. 記憶體系統的重排序

Java併發程式設計:Java記憶體模型

對於編譯器重排序,JMM的編譯器重排序規則會禁止特定型別的編譯器重排序。

對於處理器重排序,JMM的處理器重排序規則要求Java編譯器在生成指令序列時,插入特定型別的記憶體屏障指令,通過記憶體屏障指令來禁止特定型別的處理器重排序。

4. 併發程式設計模型的分類

處理器使用寫緩衝區臨時儲存向記憶體寫入的資料。

示例:

Java併發程式設計:Java記憶體模型

其內部執行過程如下所示:

Java併發程式設計:Java記憶體模型

這裡,處理器A和處理器B可以同時把共享變數寫入自己的寫緩衝區(A1,B1),然後從記憶體中讀取另一個共享變數(A2,B2),最後才把自己寫緩衝區中儲存的髒資料重新整理到記憶體中(A3,B3)。當以這種時序執行時,程式就可以得到x=y=0的結果。

為了保證記憶體可見性,Java編譯器在生成指令序列的適當位置插入記憶體屏障指令來禁止特定型別的處理器重排序。JMM的記憶體屏障指令分為以下4類:

Java併發程式設計:Java記憶體模型

5. happens-before

Java JSR-133使用happens-before概念來闡述操作之間的記憶體可見性。

與程式設計師密切相關的happens-before規則如下:

  • 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作。

  • 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。

  • volatile變數規則:對一個volatile變數的寫,happens-before於任意後續對這個volatile變數的讀。

  • 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。

happens-before與JMM的關係:

Java併發程式設計:Java記憶體模型

二、指令重排序

重排序是指編譯器和處理器為了優化程式效能而對指令序列進行重新排序的一種手段。

1. 資料依賴性

如果兩個操作訪問同一個變數,且這兩個操作中有一個寫操作,此時這兩個操作之間就存在資料依賴性。

對於資料依賴性,主要分為三種型別:

Java併發程式設計:Java記憶體模型

對於上述三種情況,只要重排序兩個操作的操作順序,程式的執行結果就會被改變。

2. as-if-serial語義

as-if-serial語義的意思是:不管怎麼重排序,單執行緒程式的執行結果不能被改變。

為了遵守as-if-serial語義,編譯器和處理器不會對存在資料依賴關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在資料依賴關係,這些操作就可能被編譯器和處理器重排序。

例如:

double pi = 3.14;         //A
double r = 1.0;           //B
double area = pi * r * r; //C

對於上述操作的資料依賴關係,如下所示:

Java併發程式設計:Java記憶體模型

其存在兩種執行順序,如下所示:

Java併發程式設計:Java記憶體模型

as-if-serial語義使單執行緒程式設計師無需擔心重排序是否會干擾到其正常執行,也無需擔心記憶體可見性問題。

3. 重排序對多執行緒的影響

示例程式碼:

class RecordExample {
    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
        }
    }
}

可能的程式執行時序圖如下:

Java併發程式設計:Java記憶體模型

三、順序一致性記憶體模型

順序一致性記憶體模型是一個理論參考模型,在設計的時候,處理器的記憶體模型和程式語言的記憶體模型都會以順序一致性記憶體模型作為參考。

1、資料競爭與順序一致性

Java記憶體模型規範對資料競爭的定義如下:

  • 在一個執行緒寫一個變數;
  • 在另一個執行緒讀同一個變數;
  • 讀和寫沒有通過同步來排序。

當程式碼中包含資料競爭時,程式的執行往往產生違反直覺的結果。如果一個多執行緒程式能正確同步,這個程式將是一個沒有資料競爭的程式。

順序一致性(Sequentially Consistent)——程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同。

2、順序一致性記憶體模型

順序一致性記憶體模型有兩大特性:

  • 一個執行緒中的所有操作必須按照程式的順序來執行;

  • 所有執行緒都只能看到一個單一的操作執行順序。在順序一致性記憶體模型中,每個操作都必須原子執行且立刻對所有執行緒可見。

順序一致性記憶體模型為程式設計師提供的檢視:

Java併發程式設計:Java記憶體模型

當多個執行緒併發執行時,上圖的開關裝置能把所有執行緒的所有記憶體讀寫操作序列化,即在順序一致性模型中,所有操作之間具有全序關係。

舉例說明:A和B兩個執行緒。

一種執行過程(同步):

Java併發程式設計:Java記憶體模型

另一種執行過程(非同步):

Java併發程式設計:Java記憶體模型

3. 同步程式的順序一致性效果

示例程式:

class SynchronizeExample {
    int a = 0;
    boolean flag = false;
    public synchronized void writer() {    // 獲取鎖
        a = 1;
        flag = true;
    }                                       // 釋放鎖
    public synchronized void reader() {     // 獲取鎖
        if (flag) {
            int i = a;
            //...
        }
    }                                        // 釋放鎖
}

兩個記憶體模型中的執行順序:

Java併發程式設計:Java記憶體模型

JMM在具體實現上的基本方針:在不改變(正確同步的)程式執行結果的前提下,儘可能地為編譯器和處理器的優化開啟方便之門。

未同步程式在兩個模型中的執行特性存在如下幾個差異:

  1. 順序一致性模型保證單執行緒內的操作會按程式的順序執行,而JMM不保證單執行緒內的操作會按程式的順序執行(重排序)。
  2. 順序一致性模型保證所有執行緒只能看到一致的操作執行順序,而JMM不保證所有執行緒能看到一致的操作執行順序。
  3. JMM不保證對64位的long型和double型變數的寫操作具有原子性,而順序一致性模型保證對所有的記憶體讀寫操作具有原子性。

匯流排仲裁 –> 匯流排事務!!!

四、volatile的記憶體語義

1、volatile的特性

volatile變數具有以下特性:

  • 可見性。

  • 原子性。對任意單個volatile變數的讀寫具有原子性,但類似volatile++這種複合操作不具有原子性。

2、volatile讀寫建立的happens-before關係

volatile對於執行緒的記憶體可見性的影響比volatile自身的特性更為重要。

從記憶體語義上來說,volatile的寫讀與鎖的釋放獲取有相同的記憶體效果。

請看下面使用volatile變數的示例程式碼:

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

假設執行緒A執行writer()方法之後,執行緒B執行reader()方法。根據happens-before規則,該過程建立的happens-before關係可以分為3類:

  • 根據程式次序規則:1 happens-before 2; 3 happens-before 4。
  • 根據volatile規則:2 happens-before 3。
  • 根據happens-before的傳遞性規則:1 happens-before 4。

因此,上述關係的圖形化表現形式如下:

Java併發程式設計:Java記憶體模型

在上圖中,每一個箭頭連結的兩個節點,代表了一個happens-before關係。黑色箭頭表示程式順序規則;橙色箭頭表示volatile規則;藍色箭頭表示組合這些規則後提供的happens-before保證。

3、volatile寫讀的記憶體語義

volatile寫的記憶體語義如下:

  • 當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值重新整理到主記憶體。

volatile讀的記憶體語義如下:

  • 當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體設定為無效。執行緒接下來將從主記憶體中讀取共享變數。

總結一下:

  • 執行緒A寫一個volatile變數,實質上是執行緒A向接下來將要讀這個volatile變數的某個執行緒發出了其對共享變數所做修改的訊息。
  • 執行緒B讀一個volatile變數,實質上是執行緒B接收了之前某個執行緒發出的在寫這個volatile變數之前對共享變數所做修改的訊息。
  • 執行緒A寫一個volatile變數,隨後執行緒B讀這個volatile變數,這個過程實質上是執行緒A通過主記憶體向執行緒B傳送訊息。

4. volatile記憶體語義的實現

為了實現volatile記憶體語義,JMM會限制編譯器重排序和處理器重排序。

規則表:

Java併發程式設計:Java記憶體模型

從上表,我們可以看出:

  • 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。其確保了volatile寫之前的操作不會被編譯器重排序到volatile寫之後

  • 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。其確保了volatile讀之後的操作不會被編譯器重排序到volatile讀之前

  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。

實現方式:記憶體屏障。

具體記憶體屏障插入策略如下:

  • 在每個volatile寫操作前插入一個StoreStore屏障。
  • 在每個volatile寫操作後插入一個StoreLoad屏障。
  • 在每個volatile讀操作後插入一個LoadLoad屏障。
  • 在每個volatile讀操作後插入一個LoadStore屏障。

    保守策略下,volatile寫插入記憶體屏障後生成的指令序列示意圖:

Java併發程式設計:Java記憶體模型

StoreStore屏障將保障上面所有的普通寫在volatile寫之前重新整理到主記憶體。volatile寫之後的StoreLoad屏障的作用是避免volatile寫與後面可能有的volatile讀寫操作重排序(保守策略)。

保守策略下,volatile讀插入記憶體屏障後生成的指令序列示意圖:

Java併發程式設計:Java記憶體模型

LoadLoad屏障用來禁止處理器把上面的volatile讀與下面的普通讀重排序。LoadStore屏障用來禁止處理器把上面的volatile讀與下面的普通寫重排序。

舉一個例子:

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;
    void readAndWrite() {
        int i = v1;    // 第一個volatile讀
        int j = v2;    // 第二個volatile讀
        a = i + j;     // 普通寫
        v1 = i + 1;    // 第一個volatile寫
        v2 = j * 2;    // 第二個volatile寫
    }
    ... //寫
}

對應的指令序列示意圖如下:

Java併發程式設計:Java記憶體模型

5. 為什麼增強volatile的記憶體語義

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

在舊的記憶體模型中,可以使用指令重排序,因此,時序圖如下所示:

Java併發程式設計:Java記憶體模型

結果:讀執行緒B執行4時,不一定能看到執行緒A在執行1時對共享變數的修改。

volatile記憶體語義增強

volatile嚴格限制編譯器和處理器對volatile變數與普通變數的重排序,確保volatile的寫讀和鎖的釋放獲取具有相同的記憶體語義。

不過,要很好地利用volatile來完成鎖機制下的併發過程,是十分困難的,一定要謹慎。

五、鎖的記憶體語義

1、鎖的釋放獲取所建立的happens-before關係

鎖是Java併發程式設計中最重要的同步機制。

示例:

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規則,該過程包含三類關係:

  • 程式次序規則:1 happens-before 2, 2 happens-before 3, 4 happens-before 5, 5 happens-before 6。

  • 監視器鎖規則:3 happens-before 4。

  • happens-before的傳遞性:2 happens-before 5。

示意圖如下:

Java併發程式設計:Java記憶體模型

在上圖中,每一個箭頭連結的兩個節點,代表了一個happens-before關係。黑色箭頭表示程式順序規則;橙色箭頭表示監視器鎖規則;藍色箭頭表示組合這些規則後提供的happens-before保證。

2、鎖的釋放和獲取的記憶體語義

當執行緒釋放鎖時,JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體中。

當執行緒獲取鎖時,JMM會把該執行緒對應的本地記憶體設定為無效,從而使得被監視器保護的臨界區程式碼必須從主記憶體中讀取變數。

總結一下:

  • 執行緒A釋放一個鎖,實質上是執行緒A向接下來將要獲取這個鎖的某個執行緒發出了執行緒A對共享變數所做修改的訊息。
  • 執行緒B獲取一個鎖,實質上是執行緒B接收了之前某個執行緒發出的在釋放這個鎖之前對共享變數所做修改的訊息。
  • 執行緒A釋放鎖,隨後執行緒B獲取這個鎖,這個過程實質上是執行緒A通過主記憶體向執行緒b傳送訊息。

3、鎖記憶體語義的實現

在這裡,我們藉助ReentrantLock的原始碼,來分析鎖記憶體語義的具體實現機制。

示例程式碼:

class ReentrantLockExample {
    int a = 0;
    ReentrantLock lock = new ReentrantLock();
    public void writer() {
        lock.lock();    //獲取鎖
        try {
            a++;
        } finally {
            lock.unlock(); //釋放鎖
        }
    }
    public void reader() {
        lock.lock();      //獲取鎖
        try {
            int i = a;
            ...
        } finally {
            lock.unlock(); //釋放鎖
        }
    }
}

ReentrantLock的實現依賴於Java同步器框架AbstractQueuedSychronizer(AQS)

基本類圖:

Java併發程式設計:Java記憶體模型

ReentrantLock分為公平鎖和非公平鎖。

公平鎖的加鎖過程

Step1: ReentrantLock.lock();
Step2: FairSync.lock();
Step3: AbstractQueuedSynchronizer.acquire(int arg);
Step4: ReentrantLock.tryAcquire(int acquires);

第四步為核心,如下:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();          //獲取鎖的開始,首先讀取volatile變數state
    if (c == 0) {
        if (isFirst(current) && 
            compareAndSetState(0, acquires)) {
             return true;   
         }
    } else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) {
            throw new Error("Maximum lock count exceeded.");
        }
        setState(nextc);
        return true;
    }
    return false;
}

核心:讀取volatile變數state。

公平鎖的解鎖過程

Step1: ReentrantLock.unlock();
Step2: AbstractQueuedSynchronizer.release(int arg);
Step3: Sync.tryRelease(int releases);

第三步為核心,如下:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread()) {
        throw new IllegalMonitorStateException();
    }
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);                //釋放鎖的最後,寫volatile變數state
    return free;
}

公平鎖在釋放鎖的最後寫volatile變數state,在獲取鎖時首先讀這個volatile變數。根據volatile的happens-before規則,釋放鎖的執行緒在寫volatile變數之前可見的共享變數,在獲取鎖的執行緒讀取同一個volatile變數後將立即變得對獲取鎖的執行緒可見。

4、concurrent包的實現

Java執行緒之間的通訊方式:

  • A執行緒寫volatile變數,隨後B執行緒讀這個volatile變數。
  • A執行緒寫volatile變數,隨後B執行緒使用CAS更新這個volatile變數。
  • A執行緒用CAS更新一個volatile變數,隨後B執行緒用CAS更新這個volatile變數。
  • A執行緒用CAS更新一個volatile變數,隨後B執行緒讀這個volatile變數。

Java併發程式設計:Java記憶體模型

六、final域的記憶體語義

對於final域,編譯器和處理器要遵守兩個重排序規則:

  • 在建構函式內對一個final域的寫入,與隨後把這個被構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序。

  • 初次讀一個包含final域的物件的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序。

七、happens-before

happens-before是JMM最核心的概念。

1、JMM的設計

在設計JMM時,需要考慮兩個關鍵因素:

  • 程式設計師對記憶體模型的使用。程式設計師希望基於一個強記憶體模型來編寫程式碼。

  • 編譯器和處理器對記憶體模型的實現。編譯器和處理器希望實現一個弱記憶體模型。

由於上述兩個因素的互相矛盾,因此需要找到一個好的平衡點:一方面,要為程式設計師提供足夠強的記憶體可見性保證;另一方面,對編譯器和處理器的限制要儘可能地放鬆。

JMM把happens-before要求禁止的重排序分為下面兩類:

  • 會改變程式執行結果的重排序;

  • 不會改變程式執行結果的重排序。

JMM對於這兩種不同性質的重排序,採取了不同的策略,如下:

  • 對於會改變程式執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序。

  • 對於不會改變程式執行結果的重排序,JMM對編譯器和處理器不做要求。

如下所示:

Java併發程式設計:Java記憶體模型

如上所示,可以得出以下兩點:

  • JMM向程式設計師提供的happens-before規則能滿足程式設計師的需求。

  • JMM對編譯器和處理器的束縛已經儘可能少。

基本原則:只要不改變程式的執行結果,編譯器和處理器怎麼優化都行。

2、happens-before的定義

定義如下:

  • 如果一個操作happens-before另一個操作,那麼第一個操作的執行結果將對第二個操作可見,而且第一個操作的執行順序排在第二個操作之前。 —— JMM對程式設計師的承諾

  • 兩個操作之間存在happens-before關係,並不意味著Java平臺的具體實現必須要按照happens-before關係指定的順序來執行。只要結果一致,重排序就不非法。 —— JMM對編譯器和處理器重排序的約束原則

3、happens-before規則

  • 程式次序規則:一段程式碼在單執行緒中執行的結果是有序的。注意是執行結果,因為虛擬機器、處理器會對指令進行重排序。雖然重排序了,但是並不會影響程式的執行結果,所以程式最終執行的結果與順序執行的結果是一致的。故而這個規則只對單執行緒有效,在多執行緒環境下無法保證正確性。

  • 鎖定規則:這個規則比較好理解,無論是在單執行緒環境還是多執行緒環境,一個鎖處於被鎖定狀態,那麼必須先執行unlock操作後面才能進行lock操作。

  • volatile變數規則:這是一條比較重要的規則,它標誌著volatile保證了執行緒可見性。通俗點講就是如果一個執行緒先去寫一個volatile變數,然後一個執行緒去讀這個變數,那麼這個寫操作一定是happens-before讀操作的。

  • 傳遞規則:提現了happens-before原則具有傳遞性,即A happens-before B , B happens-before C,那麼A happens-before C。

  • 執行緒啟動規則:假定執行緒A在執行過程中,通過執行ThreadB.start()來啟動執行緒B,那麼執行緒A對共享變數的修改在接下來執行緒B開始執行後確保對執行緒B可見。

  • 執行緒終結規則:假定執行緒A在執行的過程中,通過制定ThreadB.join()等待執行緒B終止,那麼執行緒B在終止之前對共享變數的修改線上程A等待返回後可見。

執行緒啟動規則示例:

Java併發程式設計:Java記憶體模型

執行緒終結規則示例:

Java併發程式設計:Java記憶體模型

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

1、雙重檢查鎖定

寫一個執行緒安全的單例模式:

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
        }                                              //10
        return instance;
    }
}

問題:

線上程執行到第四行,程式碼讀取到instance不為null時,instance引用的物件有可能沒有完成初始化。

2、問題的根源

示例程式碼第7行instance = new Instance();建立物件,其可分解為:

memory = allocate();  //1. 分配物件的記憶體空間
ctorInstance(memory); //2. 初始化物件
instance = memory;    //3. 設定instance指向剛分配的記憶體地址

上面2和3之間可能會被重排序。2和3之間重排序之後的執行時序(並不違反JMM規則)如下:

memory = allocate();   //1. 分配物件的記憶體空間
instance = memory;     //2. 設定instance指向剛分配的記憶體地址
                       // 注意:此時物件還未初始化
ctorInstance(memory);  //3. 初始化物件

上述過程多執行緒下併發執行的情況:

單執行緒的執行時序圖:

Java併發程式設計:Java記憶體模型

多執行緒的執行時序圖:

Java併發程式設計:Java記憶體模型

所以對於上述多執行緒情況,可以知道,執行緒B訪問目標物件時,目標物件並未進行初始化。此處就會出現問題。

Java併發程式設計:Java記憶體模型

如何解決?

兩種方法:

  • 不允許2和3重排序;
  • 允許2和3重排序,但不允許其他執行緒看到這個重排序。

3、基於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,現在沒有問題了
                }
            }
        }
    }
}

當宣告物件的引用為volatile時,2和3之間的重排序在多執行緒環境中將會被禁止。

4、基於類初始化的解決方案

JVM在類的初始化階段,會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖,這個鎖可以同步多個執行緒多同一個類的初始化。

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    public static Instance getInstance() {
        return InstanceHolder.instance;    //這裡將觸發InstancHolder類被初始化
    }
}

上述過程如下:

Java併發程式設計:Java記憶體模型

該方案的實質是:允許2和3重排序,但不允許非構造執行緒看到這個重排序。

附加:

一個類或介面被初始化的5種情況:
1. T是一個類,而且一個T型別的例項被建立;
2. T是一個類,且T中宣告的一個靜態方法被呼叫;
3. T中宣告的一個靜態欄位被賦值;
4. T中宣告的一個靜態欄位被使用,而且這個欄位不是一個常量欄位;
5. T是一個頂級類,而且一個斷言語句巢狀在T內部被執行。

類初始化的處理過程的五個階段:

  • 第一階段:通過在Class物件上同步(即獲取Class物件的初始化鎖),來控制類或介面的初始化。這個獲取鎖的執行緒會一直等待,直到當前執行緒能夠獲取到這個初始化鎖。

Java併發程式設計:Java記憶體模型

  • 第二階段:執行緒A執行類的初始化,同時執行緒B在初始化鎖對應的condition上等待。

Java併發程式設計:Java記憶體模型

  • 第三階段:執行緒A設定state=initialized,然後喚醒在condition中等待的所有執行緒。

Java併發程式設計:Java記憶體模型

  • 第四階段:執行緒B結束類的初始化處理。

Java併發程式設計:Java記憶體模型

  • 第五階段:執行緒C執行類的初始化的處理。

Java併發程式設計:Java記憶體模型

靜態內部類的載入過程:靜態內部類的載入不需要依附外部類,在使用時才載入

九、Java記憶體模型綜述

Java併發程式設計:Java記憶體模型

十、小結

本文對Java記憶體模型做了比較全面的解讀。

相關文章