天天看點

雙重檢查幾種方案

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

在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)“看到”這個重排序。