2012-02-01 12 views
4

誰もこの例が揮発性のないスレッドセーフである理由を説明できますか? computeHashCode関数は常に同じ結果を返し、副作用なし(すなわち、冪等)を有していたと仮定して実際にスレッドセーフ(揮発性なし)

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

、あなたも、同期のすべてを取り除くことができます。

// 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... 
    } 

MORE:値は(それが本当にスレッドセーフではありません)を2回に計算されている場合、私はそれを得るには、我々は気にしないでください。私はまた、ハッシュコードの計算後に作成された新しいスレッドが新しいハッシュコードを見ることが保証されているかどうかを知りたいですか?

+0

おそらく、同期化または揮発性を使用せずに何が起こるのかを検討すると、その理由がわかります。 –

+0

'Foo'は事実上不変であることが重要であることに注意してください。これは、 'hashCode()'が冪等であることを保証します。 – Kevin

答えて

6

これは薄い氷の上を歩いていますが、ここで説明します。可視性の問題は、一部のスレッドで古いバージョンと新しいバージョンが表示される可能性があることを意味します。私たちの場合、いくつかのスレッドは0を参照し、その他のスレッドはcachedHashCodeを参照してください。

hashCode()を呼び出し、cachedHashCodeを参照するスレッドは、それを返します(if (h == 0)条件が満たされない)すべてが機能します。

しかし0cachedHashCodeが既に計算されているにもかかわらず)を参照するスレッドは、それを再び再計算します。

つまり、最悪の場合、すべてのスレッドは0(最初はThreadLocalのように)というブランチに入ります。

computeHashCode()は等価です(非常に重要です)。何回か(別のスレッドによって)呼び出して、同じ変数に再度割り当てても副作用はありません。

6

ここで重要な情報は

computeHashCode関数は常に同じ結果に

を返さこれが本当であれば、computeHashCodeを効果的に不変であることが知られており、それは常になりますので、あります同じ値を指定すると、並行性の問題は発生しません。

限り、cachedHashCodeをvolatileにする。常にゼロ以外のcomputedHashCodeになるスレッドローカル変数hを割り当てて返すので、スレッド安全性が向上するまでは違いはありません。

+4

もう一つの重要な点は、クラス変数が、メモリモデルが原子的に更新されることを保証するintであることです。だから、スレッドは "古い"値0を見てハッシュコードを再計算するかもしれませんが、部分的に初期化されたint(例えば16ビット以下)は決して見えません。これは長いもののようなものではありません。 –

+0

それは本当です。 cachedHashCodeがlong/doubleだった場合は、intを持つ原子性を失うことは間違いありません。 –

0

これは、Fooがハッシュコードに貢献するフィールドに関して不変である場合にのみ当てはまります。 (「computeHashCode関数が常に同じ結果を返すと仮定して」満足するためには必要です)。

それ以外の場合は、スレッドセーフではないと私は同意しません。

  • スレッド1はHashcode()に入り、途中まで戻り、5(たとえば)の答えを返す直前に一時停止します。
  • スレッド2 computeHashCode()に入り、途中で中断します
  • スレッド3は、ハッシュコードに影響する方法でオブジェクトを変更します。
  • スレッド1が再起動し、ハッシュコードを5に設定し、オブジェクトをハッシュマップにキーとして格納します。
  • スレッド2が再起動すると、またはJVMがそのcachedHashCodeを処理するために選択した方法に応じて、後でハッシュマップ内のキーを見つけることができる場合とできない場合があり78392.

スレッド1のハッシュコードを設定します。 jvmには、不揮発性フィールドのコピーを別々に保存するオプションがあります。 Volatileは、jvmがそれを行わないことと、すべてのスレッドが常に同じ値を見ることを保証します。

4

これは、racy single-checkイディオムと呼ばれます。これは、値の計算が冪等である(毎回同じ値を返す;結果は型が不変でなければならない)、安価である(それが誤って2回以上再計算されても大丈夫なら)。これは常に

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と表示されている場合は、値を再計算します。

有効なJavaで説明したように、これは例えばString.hashCode()が実装されている方法です。