1. 程式人生 > >雙重檢查鎖定的單例模式和延遲初始化

雙重檢查鎖定的單例模式和延遲初始化

懶漢式單例模式 如果 ron 線程並發 初始 mage 同步 locate 對象創建

  有時候需要推遲一些高開銷的對象初始化操作,並且只有在使用這些對象時才進行初始化。此時,常用的可能就是延遲初始化,例如:懶漢式單例模式,但是要正確的實現線程安全的延遲初始化需要一些技巧,下面是非線程安全的示例代碼:

public class UnsafeLazyInit {
    private static Instance instance ;
    
    public static Instance getInstance(){
        if(instance == null )             //1.A線程執行
            instance = new
Instance() ; //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和鎖擁有相同的內存語義;

  

雙重檢查鎖定的單例模式和延遲初始化