Java內存模型

Java內存模型的基礎

本文是《java併發編程的藝術》一書的學習筆記

1.Java內存模型的抽象結構

1.Java線程之間的通訊由Java內存模型(JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。

2.線程之間的共享變量存儲在主內存中,每個線程都有一個私有的本地內存,本地內存存儲了該線程以讀/寫共享變量的副本。本地內存是一個抽象概念,並不真實存在。

3.Java內存模型的抽象示意圖如下


如果線程A與線程B之間要通訊的話,需要經歷下面兩個步驟。

  • 1.線程A把本地內存A中更新的共享變量刷新到主內存中去。
  • 2.線程B到主內存中去讀取線程A之前已更新過的共享變量。

2.happens-before簡介

1.從JDK5開始,Java使用新的JSR-133內存模型。JSR-133使用happens-before的概念來闡述操作之間的內存可見性。

2.在JMM中如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須存在happens-before關係。這裏的操作既可以是在一個線程內,也可以在不同線程之間。

3.與程序員密切相關的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關係,並不意味着前一個操作必須要在後一個操作之前執行! happens-before僅僅要求前一個操作(執行的結果)對後一個操作可見,且前一個操作按順序排在第二個操作之前

happens-before與JMM的關係如下:

Volatile內存語義

Volatile的特性

理解volatile特性的一個方法是把對volatile變量的單個讀/寫,看成是使用同一個鎖對這個讀/寫做了同步。下面通過具體示例說明:

假設有多個線程分別調用上面程序的3個方法,這個程序在語義上和下面的程序等價

如上面的程序所示,一個volatile變量的單個讀/寫操作,與一個普通變量的讀/寫操作是使用同一個鎖來同步,它們之間的執行效果相同。

volatile具有以下特性

  • 可見性。 當線程A對volatile變量寫入時,該變量會刷新到主內存。然後當線程B讀取該volatile變量時,會在主內存中進行讀取,避免在自身的本地線程讀取。 對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入。
  • 原子性。 對任意單個volatile變量的讀/寫具有原子性,但類似volatile++這種複合操作不具有原子性。
  • 禁止編譯器處理器指令重排序。 嚴格限制編譯器處理器對volatile變量與普通變量的重排序,確保volatile的讀/寫和鎖的釋放/獲取具有相同的內存語義。

volatile內存語義的實現

爲了實現volatile的內存語義,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。

下面是基於保守策略的JMM內存屏障插入策略。

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

鎖的內存語義

1.當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中

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

final域的內存語義

1.通過爲final域增加讀寫重排序規則,可以爲java程序員提供初始化安全保證:

只要對象是正確構造的(被構造對象的引用在構造函數中沒有“逸出”),那麼不需要使用同步(指lock和volatile的使用)就可以保證任意線程都能看到這個final域在構造函數中被初始化之後的值

2.爲什麼final引用不能從構造函數內"逸出"?

看下示例代碼:


假設線程A執行writer()方法,線程B執行reader()方法。這裏的操作2使得對象還未完成構造前就爲線程B可見。即使這裏的操作2是構造函數的最後一步,且在程序中操作2排在操作1後面,執行read()方法的線程仍然可能無法看到final域被初始化後的值,因爲這裏的操作1和操作2可能被重排序。

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

雙重檢查鎖定的由來

下面是示例代碼

上面的代碼是有問題的,問題的根源在第7行初始化的時候。
因爲初始化構造的時候會存在重排序
new DclSinglenton()初始化構造的時候可以分解如下三行僞代碼:

memory = allocate(); //1.分配對象的內存空間
ctorInstance(memory);//2.初始化對象
instance = memory;//3.設置instance指向剛分配的內存地址

多線程情況下2,3可能會重排序,如下:

memory = allocate(); //1.分配對象的內存空間
instance = memory;//3.設置instance指向剛分配的內存地址
                      //注意,此時對象還沒有被初始化
ctorInstance(memory);//2.初始化對象

第7行如果發生重排序,另一個併發執行的線程B就有可能在第4行判斷instance不爲null. 線程B接下來將訪問instance所引用的對象,但此時這個對象可能還沒有被A線程初始化!

解決方法:
1.不允許2和3重排序
2.允許2和3重排序,但不允許其他線程”看到“這個重排序。

基於volatile的解決方案

public class SafeDoubleCheckedLocking{
    private volatile static Instance instance;//聲明volatile
    public static Instance getInstance(){
        if (instance == null){
            synchronized (SafeDoubleCheckedLocking.class){
                if (instance == null){
                    instance = new Instance(); //沒有問題
                }
            }
        }
    }
}

聲明對象的引用爲volatile後,禁止僞代碼中的2,3重排序

基於類初始化的解決方案

JVM在類的初始化階段(即在Class被加載後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。

基於這個特性,可以實現另一種線程安全的延遲初始化方案

public class StaticInnerSingleton {
private StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
    return SingletonHolder.sInstance;
}
// 靜態內部類
private static class SingletonHolder {
    private static final StaticInnerSingleton sInstance = new StaticInnerSingleton();
}
}

參考:

《java併發編程的藝術》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章