2012-01-24 43 views
10

當我讀維基百科的約​​成語文章,我感到困惑的是執行:爲什麼使用單獨的包裝類實現此雙重檢查鎖?

public class FinalWrapper<T> { 
    public final T value; 
    public FinalWrapper(T value) { 
     this.value = value; 
    } 
} 
public class Foo { 
    private FinalWrapper<Helper> helperWrapper = null; 

    public Helper getHelper() { 
     FinalWrapper<Helper> wrapper = helperWrapper; 

    if (wrapper == null) { 
     synchronized(this) { 
      if (helperWrapper == null) { 
       helperWrapper = new FinalWrapper<Helper>(new Helper()); 
      } 
      wrapper = helperWrapper; 
     } 
    } 
    return wrapper.value; 
    } 
} 

我只是不明白爲什麼我們需要創建包裝。這不夠嗎?

if (helperWrapper == null) { 
    synchronized(this) { 
     if (helperWrapper == null) { 
      helperWrapper = new FinalWrapper<Helper>(new Helper()); 
     } 
    } 
}  

是因爲使用的包裝,因爲包裝存儲在堆棧和helperWrapper存儲在堆可以加快初始化?

+0

@pst:我想他問的是附加的'wrapper'局部變量,它包含'helperWrapper'副本。 – casablanca

+0

@casablanca哦,哼哼,完全錯過了這個角度....現在我很困惑,因爲問題的其他措辭。 「我不明白爲什麼我們需要創建一個包裝。」 :( – 2012-01-24 06:59:48

+0

偉大的問題的方式。歡迎來到SO! – Voo

回答

1

這不夠嗎?

if (helperWrapper == null) { 
    synchronized(this) { 
     if (helperWrapper == null) { 
      helperWrapper = new FinalWrapper<Helper>(new Helper()); 
     } 
    } 
} 

不,這是不夠的。

以上,首先檢查helperWrapper == null是不是線程安全的。它可能會返回false(看到非null實例)某個線程「太早」,指向未完全構造的helperWrapper對象。

非常Wikipedia article您參考,解釋了這個問題一步一步:

例如,請考慮以下的事件序列:

  1. 線程A通知該值不初始化,所以它獲得鎖並開始初始化該值。
  2. 由於某些編程語言的語義,編譯器生成的代碼允許在A完成 執行初始化之前將共享變量更新爲指向部分構造的對象的 。
  3. 線程B注意到共享變量已被初始化(或如此顯示),並返回其值。因爲線程B認爲 值已經初始化,所以它不會獲取該鎖。如果B使用 ,則在由A完成的所有初始化之前,對象被B 看到(或者因爲A尚未完成初始化或者因爲對象中的一些初始化值尚未過濾到內存B使用的 緩存一致性)),程序可能會崩潰。上面提到的一些編程語言的

語義是完全的Java的語義爲1.5或更高版本。 Java內存模型(JSR-133)明確允許這樣的行爲 - 如果您有興趣,可以在網絡上搜索更多細節。

這是因爲使用包裝可以加速初始化,因爲包裝存儲在堆棧和helperWrapper存儲在堆?

不,以上不是原因。

原因是線程安全。再次,Java 1的語義。5和更高版本(如Java內存模型中所定義的)確保任何線程只能從包裝程序訪問正確初始化的Helper實例,因爲它是構造函數中初始化的最終字段 - 請參閱JLS 17.5 Final Field Semantics

+1

我相信你的意思是'它可能會返回_false_某些線程...'在'helperWrapper == null'句子後的文本中。 – Cyberfox

+0

@Cyber​​fox你是對的 - 謝謝你抓住這個!我糾正了錯誤 – gnat

+0

「它可能會返回false(看到非null實例)某個線程」太早「,指向沒有完全構建的Helper實例。」< - 如何可以「完全構造實例「在這裏泄露?:( – 2012-01-24 16:28:29

-1

我的理解是,使用wrapper的原因是讀取不可變對象(所有字段都是final)是一個原子操作。

如果wrapper爲空,那麼它還不是一個不可變的對象,我們必須落入​​塊。

如果wrapper不是空的,然後我們保證有完整構建value對象,所以我們可以退貨。

在您的代碼中,null檢查可以在沒有實際讀取引用對象的情況下完成,因此不會觸發操作的原子性。即該操作可能初始化爲helperWrapper,準備將new Helper()的結果傳遞給構造函數,但構造函數尚未調用。

現在我假定你的榜樣隨後的代碼會讀return helperWrapper.value;應該觸發一個原子參考讀取,保證構造完成,但它是完全可能的(「一些編程語言的語義學」),其允許編譯器對其進行優化以不執行原子讀取,因此它會在確切正確的情況下返回不完全初始化的對象。

使用局部變量和參考副本執行屏障強制讀取和寫入是原子的,並確保代碼是線程安全的。

我相信關鍵的理解是不可變的引用讀寫是原子的,所以賦值給另一個變量是原子的,但空測試可能不是。

2

據我瞭解代碼本地wrapper確實不需要。

我的推理:Wrapper技巧首先起作用的原因是因爲JLS保證編譯器在其所有最終字段被正確初始化之前不會發布對象的引用。因此,我們保證helperWrapper/wrapper爲空或helperWrapper/wrapper.value指向正確初始化的對象。 假設最終沒有保證的額外地方不會做任何事情之一:編譯器將完全在其部分創建變量分配給兩個wrapperhelperWrapper

右因此,當地應該是多餘的,不影響代碼的正確性。

+0

在Wiki文章中,它說「局部變量包裝是正確的。」所以我仍然認爲包裝是必要的。 – Audrey

+0

@Audrey是的,因爲我們都知道維基百科從來沒有錯。到目前爲止,沒有人能夠說出爲什麼地方是必要的任何好的論點。在維基百科中引用的gnat的答案**中的時間序列不能在Java內存模型中的最終變量發生**。 – Voo

+0

@Voo簡單地使用helperWrapper進行空檢查和return語句可能會失敗,因爲在Java內存模型下允許讀取重新排序。我已經使用參考鏈接更新了Wiki文章。 –

1

由於在Java內存模型下允許讀取重新排序,因此只需使用helperWrapper進行空值檢查和return語句即可失敗。

下面是示例方案:

  1. 第一helperWrapper == null(活潑的讀出)的測試結果爲假,即helperWrapper不爲空。
  2. 最後一行return helperWrapper.value(racy read)導致NullPointerException,即helperWrapper爲空

這是怎麼發生的? Java存儲器模型允許對這兩個讀取數據進行重新排序,因爲在讀取之前沒有障礙,即沒有「發生之前」關係。 (請參閱String.hashCode example

請注意,在您讀取helperWrapper.value之前,您必須隱式閱讀helperWrapper引用本身。因此,由final語義提供的保證,helperWrapper完全實例化不適用,因爲它們只有適用於helperWrapper不爲空時。

+0

這意味着動作設置helpWrapper包裝由於最終的語義而禁止讀取重新排序? – GhostFlying

+0

@GhostFlying no。當我們編寫'return wrapper.value'時,helperWrapper的讀取不能被重新排序,因爲它不再讀取helperWrapper。 –

相關問題