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

原文地址https://www.hcyhj.cn/2018/11/21/delay-loadjava


最近開始看《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指向的是一個屬性和值都是缺省值的對象。而後被一個競爭線程所拿到並進行使用。
code


目前有兩種解決辦法

第一種:給實例變量加上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也是同一個。blog


End

相關文章
相關標籤/搜索