單例雙重加鎖


  相信面向對象程序員都對單例模式比較熟悉,而對於單例模式卻有着各種各樣的寫法,今天我專門針對一種稱為雙重加鎖的寫法進行分析。我們先來看下這種寫法。

/**
 * 單例雙重加鎖Demo
 *
 */
public class DoubleCheckLock {
   
    private static DoubleCheckLock instance ;
    
    private DoubleCheckLock(){
        
    }
    
    public static DoubleCheckLock getInstance(){
        if(instance == null){
            synchronized (DoubleCheckLock.class) {
                if(instance == null)
                    instance = new DoubleCheckLock() ;
            }
        }
        return instance;
    }
}

  這種寫法相信很多人都見過,但是你認為這種寫法是正確的嗎?或者更准確的來說,這種寫法在並發的環境下是否還能表現出正確的行為呢。

  之所以有這種所謂的雙重加鎖,一方面是因為延遲初始化可以提高性能,另一方面通過使用內置鎖sychronized來防止並發,其原理是首先檢查是否在沒有同步的情況下進行了初始化,如果沒有的話,在進行同步,然后再次檢查是否對其(instance)進行了初始化,如果沒有那么則初始化DoubleCheckLock。

  這種寫法表面看起來既提高了性能,又保證了線程安全。但實際上卻並不是如此,我只從線程安全上來分析這種寫法的對錯。

  在這,首先應該注意的是使用內置鎖加鎖的是DoubleCheckLock.class,並不是instance,也就是說沒有在instance實現同步,那么在這種情況下,當有兩個線程同時進行到synchronized代碼塊時,只有一個線程可以進入,然后初始化了instance,但是這僅僅只能保證的是兩個線程在訪問上的獨占性,也就是說兩個線程在此一定是一先一后進行訪問,但是不能保證的是instance的內存可見性,原因很簡單,因為同步的對象並不是instance,而是DoubleCheckLock.class(可以保證內存可見性)。不能保證內存可見性的后果就是當第一個線程初始化instance之后,第二個線程並不能馬上看見instance被初始化,或者更准確的來說,第二個線程看到的可能只是被部分構造的instance。因此,這種造成的后果是第二個線程讀取到了錯誤的instance的狀態,有可能instance會被再次實例化。

  那么如何解決這個問題呢,最簡單的方式是對instance加上關鍵詞volatile,volatile可以保證變量的內存可見性,同時volatile同步的消耗也非常小,這么做到話,可以保證線程安全。

  上述解決問題的方式固然是可以,但是實質上我感覺很繁瑣其代碼閱讀效果也不好,就單例而言,我推薦一下的寫法。

public class Single {
    
    private Single(){}
    
    private static class SingleHolder{
        public static Single instance = new Single();
    }
    
    public static Single getInstance(){
        return SingleHolder.instance;
    }
}

  這種寫法相對而言比較簡單,而且處理了兩個問題:1.線程安全問題。2.延遲初始化(初始化在調用getInstance的時候才會去靜態內部類中初始化instance)。而且相對而言,有着更加良好的代碼可讀性。

  對於雙重加鎖的這種寫法就先分析到這,等后面說到Happens-Before之后我會再來分下下雙重加鎖。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM