2012-02-01 61 views
4

任何人都可以解釋爲什麼這個例子是線程安全的沒有易失性?線程安全無易失性

http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

事實上,假設computeHashCode函數總是返回相同的結果,並且沒有副作用(即冪等),你甚至可以擺脫所有同步的。

// Lazy initialization 32-bit primitives 
// Thread-safe if computeHashCode is idempotent 
class Foo { 
    private int cachedHashCode = 0; 
    public int hashCode() { 
    int h = cachedHashCode; 
    if (h == 0) { 
     h = computeHashCode(); 
     cachedHashCode = h; 
     } 
    return h; 
    } 
    // other functions and members... 
    } 

更多:我明白了,如果這個值被計算兩次(所以它不是真正的線程安全),我們不關心。我也想知道在哈希碼計算後創建的新線程是否可以保證看到新的哈希碼?

+0

也許你會考慮如果不使用synchronized或volatile會發生什麼會更有用,你會明白爲什麼不需要它。 – 2012-02-01 22:02:32

+0

請注意,「Foo」實際上是不可變的,這一點很重要。這保證'hashCode()'是冪等的。 – Kevin 2016-02-17 19:28:48

回答

6

這是在薄冰上行走,但這裏是解釋。可見性問題意味着一些線程可能會看到舊版本和一些新的版本。在我們的案例中,一些線程看到0而其他線程 - cachedHashCode

線程調用hashCode()cachedHashCode將返回它(if (h == 0)條件不符合),並且一切正常。

但看到0(儘管cachedHashCode可能已被計算)的線程將重新計算它。

換句話說,在最壞的情況下,每個線程首次會看到0(如果它是ThreadLocal),將進入分支。

由於computeHashCode()是冪等(非常重要),它們都會多次調用(通過不同的線程)並將其重新分配給同一個變量應該沒有任何副作用。

6

信息的重要棋子這裏是

computeHashCode函數總是返回相同的結果

如果爲真,那麼computeHashCode被稱爲是有效不可變的,因爲它永遠是相同的價值,你永遠不會有併發問題。

至於使cachedHashCode易變。就線程安全而言,它不會有所作爲,因爲您總是分配並返回一個線程局部變量h,它將是一個非零的computedHashCode。

+4

另一個關鍵點是類變量是一個int,內存模型保證它是原子更新的。因此,儘管一個線程可能會看到「stale」0值並重新計算哈希碼,但它永遠不會看到部分初始化的int(比如說低16位)。長期以來,情況並非如此。 – 2012-02-01 21:43:46

+0

的確如此。如果cachedHashCode是一個long/double,你肯定會失去int的原子性。 – 2012-02-01 21:47:46

0

這隻有在Foo對於有助於哈希碼的字段不可變的情況下才會如此。 (必須滿足「假設computeHashCode函數總是返回相同的結果」)

否則我不同意它會是線程安全的。

  • 線程1進入Hashcode(),獲取半途,並在返回5(例如)之前暫停。
  • 線程2輸入computeHashCode()並在其中途暫停
  • 線程3以影響哈希碼的方式修改對象。
  • 線程1重新啓動,將散列碼設置爲5並將該對象放置在散列表中作爲鍵。
  • 線程2個重新啓動後,設置哈希碼78392.

線程1可能會或可能不會以後能夠找到散列圖的關鍵,取決於JVM是如何選擇來處理cachedHashCode。如果喜歡,jvm可以選擇保留非易失性字段的單獨副本。易失性僅確保jvm不會這樣做,並且所有線程始終都能看到相同的值。

4

這被稱爲活潑的單檢成語。當計算一個值是冪等的(每次返回相同的值;推論:該類型必須是不可變的)並且便宜(如果它不止一次可以重新計算),它就會被使用。這總是採取某種形式沿線

class Foo { 
    private Value cacheField; // field holding the cached value; not volatile! 

    public Value getValue() { 
    Value value = cacheField; // very important! 
    if (value == 0 or null or whatever) { 
     value = computeValue(); 
     cacheField = value; 
    } 
    return value; 
    } 
} 

或更多或更少的等價物。如果你的實現不是冪等的,或者不便宜,那麼你應該使用另一個習慣用法;詳細信息請參閱有效的Java項目71。但重點是,線程最多可以讀取到cacheField,如果他們看到cacheField處於未計算值的狀態,則重新計算該值。

正如Effective Java所述,例如,String.hashCode()就是如何實現的。