原文地址https://www.hcyhj.cn/2018/11/21/delay-load
最近開始看《java并發(fā)編程的藝術(shù)》一書,從里面get到了好些知識上的盲點,下面就延遲加載這個問題來分析一波~~
首先咱們來看一段簡單的代碼:
public class DelayLoad {
private DelayLoad() {
}
private static DelayLoad instance;
public static DelayLoad getInstance() {
if (instance == null) { //步驟1
instance = new DelayLoad(); //步驟2
}
return instance;
}
}
從上面的代碼片段里,很容易發(fā)現(xiàn)在多線程并發(fā)情況下去調(diào)用getInstance是會出問題的.當(dāng)A線程和B線程同時進入到步驟1處,便會實例化兩個對象出來,A和B訪問到的對象就不會是同一個。
下面升級一下,加上同步關(guān)鍵字synchronized
public class DelayLoad {
private DelayLoad() {
}
private static DelayLoad instance;
public static synchronized DelayLoad getInstance() {
if (instance == null) {
instance = new DelayLoad();
}
return instance;
}
}
代碼改成這樣后,可以完全保證并發(fā)情況下獲取的instance實例都會是同一個,但是多個線程同時調(diào)用synchronized 修飾的方法,會有獲取鎖以及釋放鎖操作,這里會造成大量的性能損耗,得不償失!
繼續(xù)改造一下,看能不能提升下性能:
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(); //創(chuàng)建實例
}
}
}
return instance;
}
}
咱們這里用雙重檢測的方法來實現(xiàn)這個單例懶加載,用這種策略看上去貌似沒有什么問題,多線程并發(fā)的情況下往往也就是在第一次檢查時都會直接返回實例,這樣就不會造成性能損耗.但是,這里有可能出現(xiàn)instance不一致的問題掌呜。對于這個問題我們得先了解對象的初始化過程。
對象的初始化過程
1.在堆上為DelayLoad對象分配足夠大的空間,所有屬性和方法都被設(shè)置成缺省值(數(shù)字為0详拙,字符為null,布爾為false蔓同,而所有引用被設(shè)置成null)饶辙。
2.執(zhí)行構(gòu)造函數(shù)檢查是否有父類,如果有父類會先調(diào)用父類的構(gòu)造函數(shù)斑粱,這里假設(shè)DelayLoad沒有父類弃揽,執(zhí)行缺省值字段的賦值即方法的初始化動作。
3.執(zhí)行構(gòu)造函數(shù).
上面創(chuàng)建實例的那一步在cpu上可能經(jīng)過如下操作:
memory = allocate(); //1分配對象內(nèi)存空間
initInstance(memory);//2初始化對象
instance = memory; //3設(shè)置instance指向剛分配的內(nèi)存地址
但實際上執(zhí)行的過程中,2和3步驟有可能進行指令重排,也就是按132的順序執(zhí)行,這樣就會導(dǎo)致instance指向的是一個屬性和值都是缺省值的對象。然后被一個競爭線程所拿到并進行使用矿微。
目前有兩種解決辦法
第一種:給實例變量加上volatile 關(guān)鍵字修飾
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;
}
}
代碼改成上述情況后,在設(shè)置instance指向更分配的內(nèi)存地址之前會有StoreStore內(nèi)存屏障,執(zhí)行代碼會禁止指令重排,這樣咱們拿到的instance都是經(jīng)過初始化過的痕慢。
第二種:基于類初始化的解決方案
public class DelayLoad {
private DelayLoad() {
}
private static class DelayLoadHolder {
public static DelayLoad instance = new DelayLoad();
}
public static DelayLoad getInstance() {
return DelayLoadHolder .instance;
}
}
該延遲加載方案是基于JVM的類初始化原理實現(xiàn)的。在執(zhí)行類的初始化期間涌矢,JVM會去獲取一個鎖掖举,該鎖可以同步多個線程對同一個類的初始化。類只會被加載一次,在加載完成之前對其他線程都是不可見的娜庇。這樣也能保證獲取到的instance也是同一個拇泛。