單例模式中,有一個DCL(雙重鎖)的實現方式。在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類中,假設A線程執行代碼1的同時,B線程執行代碼2。此時,線程A可能會看到instance引用對象尚未完成初始化。ide
對於UnsafeLazyInitialization類,咱們能夠對getInstance()
方法作同步處理來實現線程安全的延遲初始化。示例代碼以下。性能
/** * 安全的延遲初始化 * * @author xiaoshu */ public class SafeLazyInitialization { private static Instance instance; public synchronized static Instance getInstance() { if (null == instance) { instance = new Instance(); } return instance; } }
因爲對getInstance()
方法作了同步處理,synchronized將致使性能開銷。若是getInstance()方法被多個線程頻繁的調用,將會致使程序執行性能的降低。反之,若是getInstance()方法不會被多個線程頻繁的調用,那麼這個延遲初始化方案將能提供使人滿意的性能。優化
後來,提出了一個「聰明」的技巧:雙重檢查鎖定(Double-Checked Locking)。想經過雙重檢查鎖定來下降同步的開銷。下面是使用雙重檢查鎖定來實現延遲初始化的實例代碼。線程
/** * 雙重檢查鎖定 * * @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; } }
雙重檢查鎖定看起來彷佛很完美,但這是一個錯誤的優化!在線程執行到第1處,代碼讀取到instance不爲null時,instance引用的對象有可能尚未完成初始化。code
前面的雙重檢查鎖定實例代碼的第4處(instance = new Instance();)建立了一個對象。這一行代碼能夠分解爲以下的3行僞代碼。對象
memory = allocate(); //1.分配對象的內存空間 ctorInstance(memory); //2.初始化對象 instance = memory; //3.設置instance指向剛分配的內存地址
上面3行僞代碼中的2和3之間,可能會被重排序(在一些JIT編譯器上,這種重排序是真實發生的),2和3之間重排序以後的執行時序以下:排序
memory = allocate(); //1.分配對象的內存空間 instance = memory; //3.設置instance指向剛分配的內存地址 //注意,此時對象尚未被初始化! ctorInstance(memory); //2.初始化對象
多線程執行時序表內存
時間 | 線程A | 線程B |
---|---|---|
T1 | A1:分配對象的內存空間 | |
T2 | A3:設置instance指向內存空間 | |
T3 | B1:判斷instance是否爲空 | |
T4 | B2:因爲instance不爲null,線程B將訪問instance引用的對象 | |
T5 | A2:初始化對象 | |
T6 | A4:訪問instance引用的對象 |
在知曉了問題發生的根源以後,咱們能夠想出兩個方法來實現線程安全的延遲初始化。
1)不容許2和3重排序
2)容許2和3重排序,但不容許其餘線程「看到」這個重排序。
後文介紹的兩個解決方案,分別對應於上面這兩點。
/** * 安全的雙重檢查鎖定 * * @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,如今沒有問題了。 } } } return instance; } }
注意:這個解決方案須要JDK5或更高版本(由於從JDK5開始使用新的JSR-133內存模型規範,這個規範加強了volatile的語義)。
當聲明對象的引用爲volatile後,3行僞代碼中的2和3之間的重排序,在多線程環境中將會被禁止。
JVM在類的初始化階段(即在Class被加載後,且被線程使用以前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖.這個鎖能夠同步多個線程對同一個類的初始化。
基於這個特性,能夠實現另外一種線程安全的延遲初始化方案(這個方案被稱之爲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; //這裏將致使InstanceHolder類被初始化 } }
字段延遲初始化下降了初始化類或建立實例的開銷,但增長了訪問被延遲初始化的字段的開銷。在大多數時候,正常的初始化要優於延遲初始化。若是確實須要對實例字段使用線程安全的延遲初始化,請使用上面介紹的基於volatile的延遲初始化的方案;若是確實須要對靜態字段使用線程安全的延遲初始化,請使用上面介紹的基於類初始化的方案。