延遲載入的一些知識和誤區

mars_jun發表於2018-11-21

原文地址www.hcyhj.cn/2018/11/21/…


最近開始看《java併發程式設計的藝術》一書,從裡面get到了好些知識上的盲點,下面就延遲載入這個問題來分析一波~~


首先我們們來看一段簡單的程式碼:

public class DelayLoad {

    private DelayLoad() {
    }

    private static DelayLoad instance;

    public static DelayLoad getInstance() {
        if (instance == null) {               //步驟1
            instance = new DelayLoad();       //步驟2
        }
        return instance;
    }
}
複製程式碼

從上面的程式碼片段裡,很容易發現在多執行緒併發情況下去呼叫getInstance是會出問題的.當A執行緒和B執行緒同時進入到步驟1處,便會例項化兩個物件出來,A和B訪問到的物件就不會是同一個。


下面升級一下,加上同步關鍵字synchronized

public class DelayLoad {

    private DelayLoad() {
    }

    private static DelayLoad instance;

    public static synchronized DelayLoad getInstance() {
        if (instance == null) {
            instance = new DelayLoad();
        }
        return instance;
    }
}
複製程式碼

程式碼改成這樣後,可以完全保證併發情況下獲取的instance例項都會是同一個,但是多個執行緒同時呼叫synchronized 修飾的方法,會有獲取鎖以及釋放鎖操作,這裡會造成大量的效能損耗,得不償失!


繼續改造一下,看能不能提升下效能:

public class DelayLoad {

    private DelayLoad() {
    }

    private static DelayLoad instance;

    public static  DelayLoad getInstance() {
        if (instance == null) {                     //第一次檢查
            synchronized (DelayLoad.class){
                if (instance == null) {             //第二次檢查
                    instance = new DelayLoad();    //建立例項
                }
            }
        }
        return instance;
    }
}
複製程式碼

我們們這裡用雙重檢測的方法來實現這個單例懶載入,用這種策略看上去貌似沒有什麼問題,多執行緒併發的情況下往往也就是在第一次檢查時都會直接返回例項,這樣就不會造成效能損耗.但是,這裡有可能出現instance不一致的問題。對於這個問題我們得先了解物件的初始化過程

物件的初始化過程

1.在堆上為DelayLoad物件分配足夠大的空間,所有屬性和方法都被設定成預設值(數字為0,字元為null,布林為false,而所有引用被設定成null)。
2.執行建構函式檢查是否有父類,如果有父類會先呼叫父類的建構函式,這裡假設DelayLoad沒有父類,執行預設值欄位的賦值即方法的初始化動作。
3.執行建構函式.

上面建立例項的那一步在cpu上可能經過如下操作:

memory = allocate(); //1分配物件記憶體空間
initInstance(memory);//2初始化物件
instance = memory;  //3設定instance指向剛分配的記憶體地址
複製程式碼

但實際上執行的過程中,2和3步驟有可能進行指令重排,也就是按132的順序執行,這樣就會導致instance指向的是一個屬性和值都是預設值的物件。然後被一個競爭執行緒所拿到並進行使用。

延遲載入的一些知識和誤區

目前有兩種解決辦法

第一種:給例項變數加上volatile 關鍵字修飾

public class DelayLoad {

    private DelayLoad() {
    }

    private static volatile DelayLoad instance;

    public static  DelayLoad getInstance() {
        if (instance == null) {
            synchronized (DelayLoad.class){
                if (instance == null) {
                    instance = new DelayLoad();
                }
            }
        }
        return instance;
    }
}

複製程式碼

程式碼改成上述情況後,在設定instance指向更分配的記憶體地址之前會有StoreStore記憶體屏障,執行程式碼會禁止指令重排,這樣我們們拿到的instance都是經過初始化過的。

第二種:基於類初始化的解決方案

public class DelayLoad {

    private DelayLoad() {
    }

    private static class DelayLoadHolder {
        public static DelayLoad instance = new DelayLoad();
    }

    public static DelayLoad getInstance() {
        return DelayLoadHolder .instance;
    }
}
複製程式碼

該延遲載入方案是基於JVM的類初始化原理實現的。在執行類的初始化期間,JVM會去獲取一個鎖,該鎖可以同步多個執行緒對同一個類的初始化。類只會被載入一次,在載入完成之前對其他執行緒都是不可見的。這樣也能保證獲取到的instance也是同一個。


End

相關文章