2011-09-08 63 views
2

如果可能,我想避免鎖定讀取。但是,即使沒有涉及部分初始化的成員,這種「感覺」就像是雙重檢查鎖定。這是一個哈希映射線程安全的對象的懶惰初始化模式?

這是一個很好的構造嗎?

private final Map<String, Stuff> stash = new HashMap<String, Stuff>(); 

public Stuff getStuff(String name) { 

    if (stash.containsKey(name)) 
     return stash.get(name); 

    synchronized(stash) { 
     if (stash.containsKey(name)) { 
      return stash.get(name); 
     } 
     else { 
      Stuff stuff = StuffFactory.create(name); 
      stash.put(name, stuff); 
      return stuff; 
     } 
    } 
} 
+2

你應該使用類似番石榴的[地圖製作工具(http://guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/common/collect/MapMaker.html),除非這是爲了好玩。 –

回答

6

不,這個構造不是線程安全的。

假設線程writer正在將東西放入地圖中,並且地圖太小,必須調整大小。這是在​​區塊內完成的,所以你可能認爲你很好。

調整大小期間,地圖中的任何內容都不能保證。

現在,在同一時間,假設線程reader爲現有元素調用getStuff。此線程可以直接訪問地圖,因爲它不會在和get的首次調用​​塊。它會找到處於未定義狀態的映射,儘管它只能讀取,但它會訪問內容未定義的數據。在可能的結果是:

  • getStuff回報null當它不應該。
  • getStuff返回預期的Stuff
  • getStuff返回在調整大小期間由HashMap實現使用的一些內部對象。
  • getStuff返回一些與名稱無關的其他Stuff
  • getStuff被陷入無限循環。

這是明顯的情況,應該很容易理解。所以不行,當設計良好的課程如ConcurrentHashMap或番石榴的MapMaker時不要採取捷徑。

順便說一下:先調用containsKey然後再用get加上相同的密鑰效率相當低。只需撥打get,保存結果並將其與null進行比較。您將在地圖中保存一個搜索操作。

1

HashMap替換爲ConcurrentHashMap,您的impl很好。

更普遍的解決方案不得不擔心以下事情

1。基於名稱的鎖定,而不是全局鎖定。如果create(n1)塊,它不應該影響對其他名稱的操作。

2。如果create()返回null或拋出異常怎麼辦?有趣的是,這對於一些impl來說是個問題。

3。 create(n1)電話get(n1)怎麼辦?我們會有一個遞歸。一些impl掛起。一些impl檢測遞歸併拋出錯誤。更好IMPL應該允許遞歸來運行(其或者終止或堆棧溢出),和中間結果應該是可見的鎖定螺紋,但不可見的其他線程,直到遞歸終止。