單例模式的幾個問題(2)-DCL的缺陷

單例模式的幾個問題(2)-DCL的缺陷

  DCL模式的單例實現方式一直是各大教科書、老師、博客的推薦實現方式,可是在高併發過程當中仍存在失敗的可能性。首先先看一個使用雙重檢查的單例模式:緩存

//雙重檢查
public class Singleton4 {

    private static Singleton4 singleton4 = null;

    private Singleton4(){}

    public static Singleton4 getInstance(){
        if(singleton4 == null){
            synchronized (Singleton4.class){
                if(singleton4 == null)
                    singleton4 = new Singleton4();
            }
        }
        return singleton4;
    }

    public void doSomething(){
        System.out.println(this.getClass().getName());
    }

}

  雙檢鎖機制的出現確實是解決了多線程並行中不會出現重複new對象,並且也實現了懶加載。這樣就沒問題了嗎? 重點來了 ,因爲 new Singleton4() 不是一個原子操做,實際執行的時候須要如下幾個步驟:多線程

     1.memory=allocate();
     2.ctorInstance(memory); //對象初始化
     3.instance=memory;    //設置instance指向剛被分配的內存;併發

  執行代碼時,爲了提升性能,編譯器和處理器經常會對指令進行重排序。重排序分爲三種:編譯器重排序,指令級並行重排序,內存系統重排序,實現優化,優化結果多是函數

    1.   初始化 Singleton4 對象;高併發

    2.   把 Singleton4 對象地址賦給instance變量;性能

  也多是這樣優化

    1. 初始化一半Singleton4對象;this

    2. 把Singleton4對象地址賦給instance變量;spa

    3. 初始化剩下的Singleton4對象;線程

   若是是第二種狀況,在多線程狀況下第一個線程已經進入了  singleton4 = new Singleton4(); 正在初始化,此時已經將Singleton4對象的地址賦給instance變量,可是Singleton4對象仍爲初始化完畢。第二個線程進入函數 singleton4 == null 爲假,獲取instance對象並返回,線程若是訪問其中某些屬性或者方法可能訪問到其成員變量的不正確值。具體來講Singleton.getInstance().getSomeField()有可能返回someField的默認值0。若是程序行爲正確的話,這應當是不可能發生的事,由於在構造函數裏設置的someField的值不可能爲0。

  很顯然只要使用jmm的某種限制就可讓上面的重排序不會發生,這種重排序限制就是volatile。

  volatile有兩種內存語義,一種是緩存一致性,另外一種就是加屏障了。所謂緩存一致性就是當讀volatile變量時,jmm會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。至於屏障類型有四種,StoreStore屏障,StoreLoad屏障,LoadLoad屏障,LoadStore屏障。簡單來講,只要volatile變量於普通變量之間的重排序可能破壞volatile的內存語義,這種重排序就會被編譯器重排序規則和處理器內存屏障插入策略禁止。

    因爲有了volatile的存在,Singleton4的賦值指令不會被優化到new Singleton4()中間去,這就保證了另一個線程若是看到了Singleton4被賦值的時候,其指向的對象必定是被初始化完成的。

相關文章
相關標籤/搜索