一候学、什么是延遲初始化驯镊?
在Java多線程程序中锅锨,有時候需要采用延遲初始化來降低初始化類和創(chuàng)建對象的開銷敛苇。
延遲初始化實際上就是:當(dāng)我們要進行一些高開銷的對象初始化操作時妆绞,只有在使用這些對象時才進行初始化。最顯著的意義在于枫攀,假如程序?qū)嶋H上不會用到這些類括饶,那初始化它們的開銷就會被完全避免。
二来涨、延遲初始化的錯誤實現(xiàn)方式
1图焰、線程不安全的延遲初始化
public class UnsafeLazyInitialization {
private static Instance instance;
public static Instance getInstance() {
if (instance == null) //1:A線程執(zhí)行
instance = new Instance(); //2:B線程執(zhí)行
return instance;
}
static class Instance {
}
}
在UnsafeLazyInitialization類中,假設(shè)線程A執(zhí)行代碼1的同時蹦掐,B線程執(zhí)行代碼2技羔。此時線程A很可能看到instance引用的對象還沒有完成初始化。所以我們必須對1卧抗、2這兩步操作進行同步處理藤滥。
2、直接使用synchronized進行同步-有巨大的性能開銷
public class SafeLazyInitialization {
private static Instance instance;
public synchronized static Instance getInstance() {
if (instance == null)
instance = new Instance();
return instance;
}
static class Instance {
}
}
在早期的JVM中社裆,使用synchronized存在巨大的性能開銷拙绊,這在實際的應(yīng)用中時幾乎不可能被接受的。
3泳秀、雙重檢查鎖定-看似聰明的解決方案
public class DoubleCheckedLocking { //1
private static Instance instance; //2
public static Instance getInstance() { //3
if (instance == null) { //4:第一次檢查
synchronized (DoubleCheckedLocking.class) { //5:加鎖
if (instance == null) //6:第二次檢查
instance = new Instance(); //7:問題的根源出在這里
} //8
} //9
return instance; //10
} //11
static class Instance {
}
}
為了克服同步帶來的大量開銷标沪,人們想得到了雙重檢查鎖定這一看似聰明的解決方案。目的是僅僅對一開始競爭狀態(tài)的getInstance加鎖晶默,帶來開銷谨娜。
但由于代碼可能的重排序,直接使用上述代碼是一種錯誤的優(yōu)化磺陡。原因如下:
示例代碼第七行趴梢,即 instance = new Instance(); 可以分解為如下的三行偽代碼
memory = allocate();//1:分配對象的內(nèi)存空間
ctorInstance(memory);//2:初始化對象
instance = memory;//3:設(shè)置instance指向剛分配的內(nèi)存地址
假如2、3之間發(fā)生重排序币他,可能順序變成如下這樣
memory = allocate();//1:分配對象的內(nèi)存空間
instance = memory;//3:設(shè)置instance指向剛分配的內(nèi)存地址
ctorInstance(memory);//2:初始化對象
也就是說坞靶,當(dāng)instance已經(jīng)指向分配的內(nèi)存地址時,對象還沒有被初始化蝴悉。
我們再回到示例代碼
public class DoubleCheckedLocking { //1
private static Instance instance; //2
public static Instance getInstance() { //3
if (instance == null) { //4:第一次檢查
synchronized (DoubleCheckedLocking.class) { //5:加鎖
if (instance == null) //6:第二次檢查
instance = new Instance(); //7:問題的根源出在這里
} //8
} //9
return instance; //10
} //11
static class Instance {
}
}
當(dāng)線程A在進行代碼的第7行彰阴,即new Instance時,內(nèi)部發(fā)生了重排序拍冠,即 instance = memory 在 ctorInstance(memory) 后進行尿这。假如進程A剛剛進行到這兩步之間簇抵。而進程B恰巧在第四行(第一次檢查)處進行判斷。那么線程B判斷instance不為null射众,很可能向下進行碟摆,進而訪問instance所引用的對象。而這時進程A尚未初始化instance叨橱!從而程序發(fā)生錯誤典蜕。
三、延遲初始化的正確實現(xiàn)方式
在上面的說明中了解了問題的根源后罗洗,我們可以很容易想到兩個方法來實現(xiàn)線程安全的延遲初始化愉舔。
(1)不允許偽代碼中的2和3兩行重排序
(2)允許2和3重排序,但不允許其他線程"看到"這個重排序
1伙菜、基于volatile的解決方案
我們只要對之前雙重檢查鎖定的代碼進行一些小小的修改轩缤,就可以實現(xiàn)我們期望中的延遲初始化。
public class SafeDoubleCheckedLocking {
private volatile static Instance instance;
public static Instance getInstance() {
if (instance == null) {
synchronized (SafeDoubleCheckedLocking.class) {
if (instance == null)
instance = new Instance();//instance為volatile
}
}
return instance;
}
static class Instance {
}
}
當(dāng)instance的引用被聲明為volatile時贩绕,創(chuàng)建對象時的重排序就將在多線程環(huán)境中被禁止典奉。從而實現(xiàn)了用雙重檢查鎖定來實現(xiàn)延遲初始化。
注:這個方案需要JDK5以上版本丧叽,因為自JDK5開始使用新的JSR-133內(nèi)存模型,這個規(guī)范增強了volatile的語義公你。
2踊淳、基于類初始化鎖的解決方案
JVM在類的初始化階段(即Class被加載后,被線程使用前)陕靠,會執(zhí)行類的初始化迂尝。在執(zhí)行類的初始化期間,JVM會獲取一個鎖剪芥,這個鎖可以同步多個線程對一個類初始化垄开。基于這個特性税肪,我們可以用以下的方式來實現(xiàn)延遲初始化溉躲。
注意這個鎖是對于類的初始化,而非對象的益兄!
public class InstanceFactory {
private static class InstanceHolder {//利用這個類的初始化鎖
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance; //這里將InstanceHolder被初始化
}
static class Instance {
}
}
當(dāng)getInstance第一次被調(diào)用锻梳,發(fā)生競爭時,InstanceHolder將被初始化净捅。其中的靜態(tài)變量instance也在此時被初始化疑枯。而InstanceHolder這個類的初始化鎖保證了instance的初始化是被同步的。即無論new instance時是否發(fā)生重排序蛔六,都不會被其他線程所看到荆永。從而解決了同步問題废亭。
類的初始化鎖相關(guān)知識在此不贅述,可參考《The Art of Java Concurrency Programming》相關(guān)篇目具钥。
四豆村、兩種解決方案的對比
我們很容易可以發(fā)現(xiàn),基于類初始化鎖的方案的實現(xiàn)代碼要更加簡潔氓拼。但基于volatile的雙重檢查鎖定方案有一個額外的優(yōu)勢你画,它可以對實例字段實現(xiàn)延遲初始化。
當(dāng)我們進行延遲初始化處理時桃漾,面對實例字段我們使用基于volatile的方案坏匪,面對靜態(tài)字段我們使用集于類初始化鎖的方案。
2021-1-16