44

從頭部首先設計模式的書,以雙重檢查鎖定Singleton模式已經被如下實施:爲什麼揮發性使用在這個例子中雙重檢查鎖定

public class Singleton { 
    private volatile static Singleton instance; 
    private Singleton() {} 
    public static Singleton getInstance() { 
     if (instance == null) { 
      synchronized (Singleton.class) { 
       if (instance == null) { 
        instance = new Singleton(); 
       } 
      } 
     } 
     return instance; 
    } 
} 

我不明白爲什麼volatile是被用過的。 volatile的使用不符合使用雙重檢查鎖定的目的,即性能?

+4

我以爲雙重檢查鎖定被打破,有人修復它嗎? –

+4

對於它的價值,我發現Head First設計模式是一本可以借鑑的可怕書。當我回顧它時,現在我已經在其他地方學習過模式,但是如果不知道它真的沒有達到它的目的,就會學習它。但它很受歡迎,所以也許只是我變得密集。 :-) – corsiKa

+0

@DavidHeffernan我已經看到這個例子被用作可以信任jvm來執行DCL的一種方式。 –

回答

47

瞭解爲什麼需要volatile的好資源來自JCIP書。維基百科也有該材料的decent explanation

真正的問題是Thread A可能會在instance完成構建instance之前爲其分配一個內存空間。 Thread B將看到該作業並嘗試使用它。這導致Thread B失敗,因爲它使用的是instance的部分構建版本。

+0

好的,它看起來像一個新的volatile實現修復了DCL的內存問題。我仍然沒有得到的是在這裏使用volatile的性能意義。從我讀的volatile中可以看出,與同步一樣慢,爲什麼不只是同步整個getInstance()方法調用? – toc777

+4

@ toc777'volatile'比通常的字段慢。如果你尋找表現,去持有者級的模式。 'volatile'在這裏僅僅是爲了表明*有一種方法*來讓破碎的圖案起作用。這更多的是編碼挑戰而不是真正的問題。 – alf

+0

@alf解釋了很多,謝謝。他們在書中根本沒有說清楚。 – toc777

0

如果你沒有它,第二個線程會在第一個線程設置爲null後進入synchronized塊,並且你的本地緩存仍然認爲它是空的。

第一個不是爲了正確(如果它是你是正確的,它會自我挫敗),而是爲了優化。

1

聲明變量爲volatile可確保對其進行的所有訪問都實際從內存中讀取其當前值。

如果沒有volatile,編譯器可能優化掉內存訪問並將其值保存在一個寄存器中,所以只有變量的第一次使用才能讀取包含該變量的實際內存位置。如果變量被第一次訪問和第二次訪問之間的另一個線程修改,則會出現問題;第一個線程只有第一個(預先修改)值的副本,所以第二個if語句測試變量值的陳舊副本。

+3

-1今天我失去了我的聲譽點:) _real_的原因是,有內存緩存,建模爲線程的本地內存。本地內存刷新到主內存的順序是未定義的 - 也就是說,除非你有*發生 - 關係之前,例如通過使用volatile。寄存器與不完整的對象和DCL問題無關。 – alf

+3

您對'volatile'的定義太狹隘 - 如果這一切都是波動的,那麼雙重檢查鎖定在 Voo

+1

@TimBender如果單身_contains_可變狀態,沖洗它與單身人士本身的引用無關(嗯,有一個間接的鏈接,作爲訪問一個單引號的'volatlie'引用會讓你的線程重新讀取主內存 - 但這是一個次要的效果,而不是問題的原因:)) – alf

4

那麼,沒有雙重檢查鎖定性能。這是一個損壞的模式。

離開感情放在一邊,volatile是在這裏,因爲沒有它的第二個線程經過instance == null的時候,第一個線程可能不會構建new Singleton()尚未:沒有人承諾,對象的創建之前發生分配instance任何線程,但實際創建對象的那個。

volatile依次建立發生在之間的讀寫關係,並修復損壞的模式。

如果您正在尋找性能,請使用holder內部靜態類。

+0

固定,謝謝@卡波爾! – alf

1

易失性讀取本身並不昂貴。

您可以設計一個測試,在嚴密的循環中調用getInstance(),以觀察易失性讀取的影響;然而那個測試是不現實的;在這種情況下,程序員通常會調用getInstance()一次,並在使用期間緩存實例。

另一個impl是通過使用final字段(請參見維基百科)。這需要額外讀取,這可能會比volatile版本更昂貴。在緊密的循環中,版本可能會更快,然而,該測試如前所述是毫無意義的。

9

正如@irreputable引用的那樣,volatile不是很貴。即使價格昂貴,一致性應該優先於性能。

Lazy Singletons還有一種更乾淨優雅的方式。

public final class Singleton { 
    private Singleton() {} 
    public static Singleton getInstance() { 
     return LazyHolder.INSTANCE; 
    } 
    private static class LazyHolder { 
     private static final Singleton INSTANCE = new Singleton(); 
    } 
} 

來源的文章:Initialization-on-demand_holder_idiom從維基百科

在軟件工程中,按需持有人的初始化(設計模式)成語是一個懶惰的加載單。在Java中的所有版本,成語支持與良好的性能

安全,高併發延遲初始化

由於類沒有任何static變量初始化,初始化完成平凡。

其中的靜態類定義LazyHolder不會被初始化,直到JVM確定必須執行LazyHolder。

靜態類LazyHolder僅在類Singleton上調用靜態方法getInstance時執行,並且第一次發生時,JVM將加載並初始化LazyHolder類。

該解決方案是線程安全的,不需要特殊的語言結構(即volatile或​​)。

相關問題