雙重檢查鎖定

看 "java併發編程的藝術" 第3.8
java

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

  在Java多線程程序中,有時候須要採用延遲初始化來下降初始化類和建立對象的開銷。雙
重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法。本文將分析雙重檢查鎖定的
錯誤根源,以及兩種線程安全的延遲初始化方案。程序員

須要注意的是, 雙重檢查鎖定自己是錯誤的!編程

雙重檢查鎖定的由來

  在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引用的對象尚未完成初始化(出現這種狀況的緣由見3.8.2節)。
  對於UnsafeLazyInitialization類,咱們能夠對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()方法不會被
多個線程頻繁的調用,那麼這個延遲初始化方案將能提供使人滿意的性能。
  在早期的JVM中,synchronized(甚至是無競爭的synchronized)存在巨大的性能開銷。所以,
人們想出了一個「聰明」的技巧:雙重檢查鎖定(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帶來的性能開銷。上面代碼表面上看起來,彷佛兩全
其美。
  多個線程試圖在同一時間建立對象時,會經過加鎖來保證只有一個線程能建立對象。
·在對象建立好以後,執行getInstance()方法將不須要獲取鎖,直接返回已建立好的對象。
雙重檢查鎖定看起來彷佛很完美,但這是一個錯誤的優化!在線程執行到第4行,代碼讀
取到instance不爲null時,instance引用的對象有可能尚未完成初始化。性能

 

3.8.2 問題的根源

前面的雙重檢查鎖定示例代碼的第7行(instance=new Singleton();)建立了一個對象。這一
行代碼能夠分解爲以下的3行僞代碼。優化

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

上面3行僞代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實
發生的,詳情見參考文獻1的「Out-of-order writes」部分)。2和3之間重排序以後的執行時序如
下。spa

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

  

  上面3行僞代碼的2和3之間雖然被重排序了,但這個重排序
並不會違反intra-thread semantics。這個重排序在沒有改變單線程程序執行結果的前提下,能夠
提升程序的執行性能。
爲了更好地理解intra-thread semantics,請看如圖3-37所示的示意圖(假設一個線程A在構
造對象後,當即訪問這個對象)。
如圖3-37所示,只要保證2排在4的前面,即便2和3之間重排序了,也不會違反intra-thread
semantics。
下面,再讓咱們查看多線程併發執行的狀況。如圖線程

相關文章
相關標籤/搜索