雙重檢查鎖定的單例模式和延遲初始化
有時候需要推遲一些高開銷的對象初始化操作,並且只有在使用這些對象時才進行初始化。此時,常用的可能就是延遲初始化,例如:懶漢式單例模式,但是要正確的實現線程安全的延遲初始化需要一些技巧,下面是非線程安全的示例代碼:
public class UnsafeLazyInit { private static Instance instance ; public static Instance getInstance(){ if(instance == null ) //1.A線程執行 instance = newInstance() ; //2.B線程執行 return instance ; } }
在示例代碼中,假如A線程執行步驟1的同時,B線程執行步驟2,線程A可能會看到instance引用的對象還沒有初始化完成。
我們可以對getInstance()方法做同步處理來實現線程安全的延遲初始化。示例代碼如下:
public class UnsafeLazyInit { private static Instance instance ; public synchronized static Instance getInstance(){if(instance == null ) instance = new Instance() ; return instance ; } }
對getInstance()方法加上了synchronized關鍵字進行同步處理,這將導致線程獲取鎖和釋放鎖的開銷,並且線程之間競爭鎖會造成阻塞。如果getInstance()方法不會被多個線程頻繁調用,那麽這個方案也能夠提供令人滿意的性能。如果需要多線程頻繁的調用,將會導致線程執行性能下降。
進一步改進,可以使用雙重檢查鎖定來實現延遲初始化。示例代碼如下:
public class UnsafeLazyInit { private static Instance instance ; //1 //2 public synchronized static Instance getInstance(){ //3 if(instance == null ){ //4.第一次檢查 synchronized(UnsafeLazyInit.class){ //5.加鎖 if(instance ==null ) //6.第二次檢查 instance = new Instance(); //7.初始化對象: 問題的根源 } } return instance ; } }
如上代碼所示,如果第一次檢查結果不為null,那麽就不需要進行加鎖和初始化操作 。因此,可以大幅度降低synchronized帶來的性能開銷,看起來似乎兩全其美:當多個線程試圖在同一時間創建一個對象時,第5步代碼通過加鎖保證了只有一個線程能夠創建對象。
在對象創建好之後,執行getInstance()方法將不需要再次獲得鎖,直接返回創建的對象。
但是以上代碼還有一個錯誤的優化!當線程A執行到第7步時,線程B執行到第4步,這時候線程B讀取到的instance可能不為null,但是instance的引用卻還沒完成初始化。
在第7步創建一個對象,可以拆分為以下三行偽代碼執行:
1. memory = allocate() ;//分配對象的內存空間 2. ctorInstance(memory) ;//初始化對象 3. instance = memory ;//引用指向內存空間
上述的偽代碼,可能會被重排序,在JMM中,這種重排序是被允許的,它只保證重排序不會改變對單線程的執行結果,上述代碼2、3步驟重排序不會影響單線程的執行結果,重排序之後的執行順序如下:
1. memory = allocate() ;//分配對象的內存空間 3. instance = memory ;//引用指向內存空間 //註意: 還沒有初始化 2. ctorInstance(memory) ;//初始化對象
如果是單線程訪問,重排序並不會影響最後的執行結果,如下圖所示:
下圖表示多線程並發執行的情況:
如上圖,重排序只能保證線程A能夠正確的訪問對象,線程B可能訪問到一個還沒初始化完成的對象。
在知曉了問題的根源之後,要實現線程安全的延遲加載,可以考慮以下兩點:
(1)不允許2和3重排序。
(2)允許2和3重排序,但是這個重排序對其他線程不可見。
基於volatile的解決方案:
只需要把以上示例的代碼做一點小修改(instance聲明為volatile型),就可以實現線程安全的延遲初始化。示例代碼如下:
public class UnsafeLazyInit { private volatile static Instance instance ; public synchronized static Instance getInstance(){ if(instance == null ){ synchronized(UnsafeLazyInit.class){ if(instance ==null ) instance = new Instance(); //instance為volatile,會插入內存屏障,禁止重排序 } } return instance ; } }
註意:以上方法需要JDK5或者更高的版本,JDK5之後使用新的內存模型JSR-133內存模型,增強了volatile的內存語義,使volatile和鎖擁有相同的內存語義;
雙重檢查鎖定的單例模式和延遲初始化