雙重鎖的由來
單例模式中,有一個DCL(雙重鎖)的實現(xiàn)方式玄柠。在Java程序中,有時候可能需要推遲一些高開銷的對象初始化操作,并且只有在使用這些對象時才開始初始化岖食。
下面是非線程安全的延遲初始化對象的實例代碼岩齿。
/**
* @author xiaoshu
*/
public class Instance {
}
/**
* 非線程安全的延遲初始化對象
*
* @author xiaoshu
*/
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (null == instance) {
instance = new Instance();
}
return instance;
}
}
在UnsafeLazyInitialization類中入篮,假設(shè)A線程執(zhí)行代碼1的同時谨垃,B線程執(zhí)行代碼2。此時墓拜,線程A可能會看到instance引用對象還沒有完成初始化港柜。
對于UnsafeLazyInitialization類,我們可以對getInstance()
方法做同步處理來實現(xiàn)線程安全的延遲初始化咳榜。示例代碼如下夏醉。
/**
* 安全的延遲初始化
*
* @author xiaoshu
*/
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (null == instance) {
instance = new Instance();
}
return instance;
}
}
由于對getInstance()
方法做了同步處理,synchronized將導(dǎo)致性能開銷涌韩。如果getInstance()方法被多個線程頻繁的調(diào)用授舟,將會導(dǎo)致程序執(zhí)行性能的下降。反之贸辈,如果getInstance()方法不會被多個線程頻繁的調(diào)用,那么這個延遲初始化方案將能提供令人滿意的性能肠槽。
后來擎淤,提出了一個“聰明”的技巧:雙重檢查鎖定(Double-Checked Locking)。想通過雙重檢查鎖定來降低同步的開銷秸仙。下面是使用雙重檢查鎖定來實現(xiàn)延遲初始化的實例代碼嘴拢。
/**
* 雙重檢查鎖定
*
* @author xiaoshu
*/
public class DoubleCheckedLocking {
private static Instance instance;
public static Instance getInstance() {
if (null == instance) { //1.第一次檢查
synchronized (DoubleCheckedLocking.class) { //2.加鎖
if (null == instance) { //3:第二次檢查
instance = new Instance(); //4.問題的根源出在這里
}
}
}
return instance;
}
}
雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的優(yōu)化寂纪!在線程執(zhí)行到第1處席吴,代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化捞蛋。
問題的根源
前面的雙重檢查鎖定實例代碼的第4處(instance = new Instance();)創(chuàng)建了一個對象孝冒。這一行代碼可以分解為如下的3行偽代碼。
memory = allocate(); //1.分配對象的內(nèi)存空間
ctorInstance(memory); //2.初始化對象
instance = memory; //3.設(shè)置instance指向剛分配的內(nèi)存地址
上面3行偽代碼中的2和3之間拟杉,可能會被重排序(在一些JIT編譯器上庄涡,這種重排序是真實發(fā)生的),2和3之間重排序之后的執(zhí)行時序如下:
memory = allocate(); //1.分配對象的內(nèi)存空間
instance = memory; //3.設(shè)置instance指向剛分配的內(nèi)存地址
//注意搬设,此時對象還沒有被初始化穴店!
ctorInstance(memory); //2.初始化對象
多線程執(zhí)行時序表
時間 | 線程A | 線程B |
---|---|---|
T1 | A1:分配對象的內(nèi)存空間 | |
T2 | A3:設(shè)置instance指向內(nèi)存空間 | |
T3 | B1:判斷instance是否為空 | |
T4 | B2:由于instance不為null撕捍,線程B將訪問instance引用的對象 | |
T5 | A2:初始化對象 | |
T6 | A4:訪問instance引用的對象 |
在知曉了問題發(fā)生的根源之后,我們可以想出兩個方法來實現(xiàn)線程安全的延遲初始化泣洞。
1)不允許2和3重排序
2)允許2和3重排序忧风,但不允許其他線程“看到”這個重排序。
后文介紹的兩個解決方案球凰,分別對應(yīng)于上面這兩點狮腿。
解決方案一:基于volatile的解決方案
/**
* 安全的雙重檢查鎖定
*
* @author xiaoshu
*/
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (null == instance) {
synchronized (SafeDoubleCheckedLocking.class) {
if (null == instance) {
instance = new Instance();//instance為volatile,現(xiàn)在沒有問題了弟蚀。
}
}
}
return instance;
}
}
注意:這個解決方案需要JDK5或更高版本(因為從JDK5開始使用新的JSR-133內(nèi)存模型規(guī)范蚤霞,這個規(guī)范增強了volatile的語義)。
當聲明對象的引用為volatile后义钉,3行偽代碼中的2和3之間的重排序昧绣,在多線程環(huán)境中將會被禁止。
解決方案二:基于類初始化的解決方案
JVM在類的初始化階段(即在Class被加載后捶闸,且被線程使用之前)夜畴,會執(zhí)行類的初始化。在執(zhí)行類的初始化期間删壮,JVM會去獲取一個鎖.這個鎖可以同步多個線程對同一個類的初始化贪绘。
基于這個特性,可以實現(xiàn)另一種線程安全的延遲初始化方案(這個方案被稱之為Initialization On Demand Holder idiom)央碟。
/**
* 基于類初始化的解決方案
*
* @author xiaoshu
*/
public class InstanceFactory {
private static class InstanceHolder {
private static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance; //這里將導(dǎo)致InstanceHolder類被初始化
}
}
字段延遲初始化降低了初始化類或創(chuàng)建實例的開銷税灌,但增加了訪問被延遲初始化的字段的開銷。在大多數(shù)時候亿虽,正常的初始化要優(yōu)于延遲初始化菱涤。如果確實需要對實例字段使用線程安全的延遲初始化,請使用上面介紹的基于volatile的延遲初始化的方案洛勉;如果確實需要對靜態(tài)字段使用線程安全的延遲初始化粘秆,請使用上面介紹的基于類初始化的方案。
參考:
- 《Java并發(fā)編程的藝術(shù)》