雙重檢查鎖定與延遲初始化
在Java多線程程式中,有時候需要採用延遲初始化來降低初始化類和建立對象的開銷。雙重檢查鎖定是常見的延遲初始化技術。
下面我們看一個非安全執行緒的延遲初始化對象的例子:
public class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) // 1:A線程執行 instance = new Singleton(); // 2:B線程執行 return instance; }}
假設A線程執行代碼1的同時,B線程執行代碼2。此時,線程A可能會看到instance引用的對象還沒有完成初始化
下面我們對上面的代碼改造一下,讓他變成安全執行緒
public class Singleton { private static Singleton instance; public static synchronized Singleton getInstance() { if (instance == null) // 1:A線程執行 instance = new Singleton(); // 2:B線程執行 return instance; }}
由於對getInstance()方法做了同步處理,synchronized將導致效能開銷。如果getInstance()方法被多個線程頻繁的調用,將會導致程式執行效能的下降(一般情況在我們項目中提供的單例總是被頻繁的調用)。反之,如果getInstance()方法不會被多個線程頻繁的調用,那麼這個延遲初始化方案將能提供令人滿意的效能。
下面我們進一步通過雙重檢查鎖定來降低同步的開銷,代碼如下:
public class Singleton { private static Singleton instance; public static Singleton getInstance() { // 第一次檢查 if(instance == null){ // 加鎖 synchronized(Singleton.class){ if (instance == null) //分配記憶體空間、初始化對象、instance指向分配的記憶體位址 instance = new Singleton(); } } return instance; }}
上面的代碼真的能保證單例嗎。讓我們來分析下
如上面代碼所示,如果第一次檢查instance不為null,那麼就不需要執行下面的加鎖和初始化操作。因此,可以大幅降低synchronized帶來的效能開銷。上面代碼錶面上看起來,似乎兩全其美。多個線程試圖在同一時間建立對象時,會通過加鎖來保證只有一個線程能建立對象。在對象建立好之後,執行getInstance()方法將不需要擷取鎖,直接返回已建立好的對象。雙重檢查鎖定看起來似乎很完美,但這是一個錯誤的最佳化。在代碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化。
讓我們來繼續分析
前面的雙重檢查鎖定範例程式碼(instance=new Singleton();)建立了一個對象。這一行代碼可以分解為如下的3行虛擬碼。
memory = allocate(); // 1:指派至的記憶體空間ctorInstance(memory); // 2:初始化對象instance = memory; // 3:設定instance指向剛分配的記憶體位址
面3行虛擬碼中的2和3之間,可能會被重排序2和3之間重排序之後的執行時序如下。
memory = allocate(); // 1:指派至的記憶體空間instance = memory; // 3:設定instance指向剛分配的記憶體位址;注意,此時對象還沒有被初始化。ctorInstance(memory); // 2:初始化對象
下面讓我們看一下多線程並發的執行情況
由於單線程內要遵守intra-thread semantics,從而能保證A線程的執行結果不會被改變。但是,當線程A和B按圖3-38的時序執行時,B線程將看到一個還沒有被初始化的對象。
基於上面的問題現象我們有2種解決方案
1、不允許2和3重排序
2、允許2和3重排序,但是不允許對其他線程“看到”這個重排序
方案一:基於volatile解決方案
只需要前面基於雙重檢查鎖定來實現的延遲方案,把instance改成volatile(JDK1.5以上支援)
public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { // 第一次檢查 if(instance == null){ // 加鎖 synchronized(Singleton.class){ if (instance == null) //分配記憶體空間、初始化對象、instance指向分配的記憶體位址 instance = new Singleton(); } } return instance; }}
當聲明為volatile以後2和3重排將被禁止,代碼將按照如下順序執行執行
方案二:基於類初始化的解決方案
JVM在類的初始化階段(即在Class被載入後,且被線程使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去擷取一個鎖。這個鎖可以同步多個線程對同一個類的初始化。基於這個特性,可以實現另一種安全執行緒的延遲初始化方案
public class Singleton { private static class InstanceHolder { public static Singleton instance = new Singleton(); } public static Singleton getInstance() { // 這裡將導致InstanceHolder類被初始化 return InstanceHolder.instance; }}
假設兩個線程並發執行getInstance()方法,下面是執行的示意圖