雙重檢查加鎖 被熟知為“懶漢式”單例模式的實(shí)現(xiàn),下文將統(tǒng)一稱之為 DCL。
早期 JVM 中因?yàn)橥降拈_銷巨大,為了降低實(shí)現(xiàn)單例模式中同步帶來(lái)的開銷,人們想出了很多技巧,DCL 便是其中一種。一段常規(guī)的 DCL 實(shí)現(xiàn)的單例模式如下:
public class DoubleCheckedLocking {
private static SingleInstance singleInstance;
public static SingleInstance getInstance() {
if (singleInstance == null) {
synchronized (DoubleCheckedLocking.class) {
if (singleInstance == null) {
singleInstance == new SingleInstance();
}
}
}
}
}
1. 描述
DCL 主要是為了使用延遲初始化來(lái)降低“餓漢式”單例模式對(duì) JVM 啟動(dòng)時(shí)的性能影響,為了方便解釋其中存在的問題,這里將上面代碼塊中的兩個(gè) if 語(yǔ)句分別成為 if.1 和 if.2。
2. 情景
假設(shè)此時(shí)有兩個(gè)線程 A 和 B 都需要獲取 SingleInstance,并且 A 線程進(jìn)入到 if.2 內(nèi),B 線程剛開始 if.1 的判斷。也就是說,在 A 線程在執(zhí)行初始化的過程中,B 線程讀取了線程間共享變量(singleInstance)。這種情況下,B 線程很有可能讀取到一個(gè)初始化并未完成的非空的引用(singleInstance 被部分構(gòu)造,產(chǎn)生原因后面會(huì)有討論)。
比如 SingleInstance 對(duì)象中有一個(gè)字段 id:String,并且在構(gòu)造函數(shù)中為該字段進(jìn)行賦值。在上文描述的情況下就是:singleInstance 實(shí)例已經(jīng)是一個(gè)非空的引用,new SingleInstance("110") 操作已經(jīng)生效,但是并沒有全部完成。singleInstance.getId() 返回的仍然是 null,而不是構(gòu)造函數(shù)中傳入的 ”110“。
這種情況下,B 線程不會(huì)再創(chuàng)建重復(fù)的實(shí)例,但是會(huì)拿著一個(gè)無(wú)效的對(duì)象進(jìn)行后續(xù)操作,可能會(huì)在程序中造成更嚴(yán)重的錯(cuò)誤。
3. 為什么會(huì)出現(xiàn)部分構(gòu)造的對(duì)象
簡(jiǎn)單來(lái)說是因?yàn)闊o(wú)序?qū)懭耄╫ut-of-order writes)。
如果構(gòu)造函數(shù)寫入非 final 字段,則不必立即將它們提交到內(nèi)存,甚至可以在單例變量之后提交。構(gòu)造函數(shù)其實(shí)已經(jīng)完成,但這并不意味著所有寫入對(duì)其它線程可見。
部分構(gòu)造就是這種情況的一個(gè)糟糕體現(xiàn),singleInstance 引用已對(duì)其它線程可見,但對(duì)象的內(nèi)容singleInstance.getId() 對(duì)其它線程并不可見。就是因?yàn)閷?duì)象構(gòu)造過程中一系列指令寫入內(nèi)存的亂序,導(dǎo)致了失效對(duì)象的產(chǎn)生。
4. 解決方法
在 Java 5.0 之后,使用 volatile 來(lái)修飾 singleInstance 實(shí)例,就不會(huì)產(chǎn)生指令重排序的情況,這樣 DCL 也就可以正常工作了。
但因?yàn)橛辛烁臃奖闩c安全的替代方式,DCL 也沒有什么特別的優(yōu)勢(shì),便被廢棄了。
5. 延遲初始化占位類模式
使用延遲初始化占位類模式,可以在保證延遲加載優(yōu)點(diǎn)的同時(shí),得到 Java 語(yǔ)言層面提供的安全保障。有興趣的同學(xué)可以搜一下,資料很多。當(dāng)然也包括 Java 內(nèi)存模型相關(guān),可以了解到更多 out-of-order writes 相關(guān)的原理。