雙重檢查幾種方案

​ 有時候需要採用延遲初始化來降低初始化類和創建對象的開銷。雙重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法。

​ 在Java程序中,有時候可能需要推遲一些高開銷的對象初始化操作,並且只有在使用這些對象時才進行初始化。此時,程序員可能會採用延遲初始化。

synchronized將導致性能開銷

public class SafeLazyInitialization {
    private static Instance instance;
    public synchronized static Instance getInstance() {
        if (instance == null)
            instance = new Instance();
        return instance;
    }
}

錯誤的優化

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
}

在線程執行到第4行,代碼讀取到instance不爲null時,instance引用的對象有可能還沒有完成初始化。

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

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

上面3行僞代碼中的2和3之間,可能會被重排序,重排序不會改變單線程內的程序執行結果。

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

在知曉了問題發生的根源之後,我們可以想出兩個辦法來實現線程安全的延遲初始化。

1)不允許2和3重排序。

2)允許2和3重排序,但不允許其他線程“看到”這個重排序。

基於volatile的解決方案

對於前面的基於雙重檢查鎖定來實現延遲初始化的方案(指DoubleCheckedLocking示例代碼),只需要做一點小的修改(把instance聲明爲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,現在沒問題了
            }
        }
        return instance;
    }
}

基於類初始化的解決方案

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

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }
    public static Instance getInstance() {
        return InstanceHolder.instance ;  // 這裏將導致InstanceHolder類被初始化
    }
}

這個方案的實質是:允許僞代碼中的2和3重排序,但不允許非構造線程(這裏指線程B)“看到”這個重排序。

發佈了311 篇原創文章 · 獲贊 114 · 訪問量 39萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章