web-dev-qa-db-ja.com

HashMapは異なるキーに対してスレッドセーフですか?

HashMapにアクセスする2つの複数のスレッドがあり、それらが同時に同じキーにアクセスしないことを保証する場合、それでも競合状態につながる可能性がありますか?

74
hsribei

@dotsidの答えで彼はこう言っています:

HashMapを何らかの方法で変更すると、コードは単純に壊れます。

彼は正しい。同期なしで更新されるHashMapは、スレッドが互いに素なキーのセットを使用している場合にevenを壊します。間違った方向に進む可能性のあるものを以下に示します。

  • 1つのスレッドがputを実行すると、別のスレッドがハッシュマップのサイズの古い値を見る場合があります。

  • スレッドがputを実行してテーブルの再構築をトリガーすると、別のスレッドが、ハッシュテーブル配列参照の一時的または古いバージョン、サイズ、内容、またはハッシュチェーンを見る場合があります。カオスが発生する可能性があります。

  • スレッドが他のスレッドが使用するキーと衝突するキーに対してputを実行し、後者のスレッドがそのキーに対してputを実行すると、後者は古いコピーを見る可能性がありますハッシュチェーン参照。カオスが発生する可能性があります。

  • 1つのスレッドが他のスレッドのキーの1つと衝突するキーを使用してテーブルをプローブすると、チェーン上のそのキーに遭遇する場合があります。そのキーでequalsを呼び出します。スレッドが同期されていない場合、equalsメソッドはそのキーで古い状態に遭遇する可能性があります。

また、2つのスレッドがputまたはremove要求を同時に実行している場合、競合状態が発生する可能性が多数あります。

私は3つの解決策を考えることができます:

  1. ConcurrentHashMapを使用します。
  2. 通常のHashMapを使用しますが、外部で同期します。例えばプリミティブミューテックス、Lockオブジェクトなどを使用します。
  3. スレッドごとに異なるHashMapを使用します。スレッドが実際にばらばらのキーのセットを持っている場合、それらは単一のマップを共有する必要はありません(アルゴリズムの観点から)。実際、ある時点でマップのキー、値、またはエントリを反復するスレッドがアルゴリズムに含まれている場合、単一のマップを複数のマップに分割すると、処理のその部分の大幅な高速化が可能になります。
85
Stephen C

ConcurrentHashMapを使用するだけです。 ConcurrentHashMapは、さまざまなハッシュバケットをカバーする複数のロックを使用して、ロックが競合する可能性を減らします。競合していないロックを取得すると、パフォーマンスにわずかな影響があります。

元の質問に答えるには:javadocによると、マップの構造が変わらない限り、問題ありません。これは、要素をまったく削除せず、まだマップにない新しいキーを追加しないことを意味します。既存のキーに関連付けられた値を置き換えることは問題ありません。

複数のスレッドがハッシュマップに同時にアクセスし、少なくとも1つのスレッドが構造的にマップを変更する場合、外部で同期する必要があります。 (構造変更は、1つ以上のマッピングを追加または削除する操作です。インスタンスに既に含まれているキーに関連付けられた値を変更するだけでは、構造変更は行われません。)

可視性については保証しませんが。そのため、時折、古くなった関連付けの取得を受け入れる必要があります。

26
Tim Bender

「アクセス」での意味に依存します。読んでいるだけなら、「 happens-before 」ルールの下でデータの可視性が保証されている限り、同じキーでも読むことができます。これは、読者がHashMapへのアクセスを開始する前に、HashMapを変更せず、すべての変更(初期構成)を完了する必要があることを意味します。

HashMapを何らかの方法で変更すると、コードは単純に壊れます。 @Stephen Cはその理由を非常によく説明しています。

編集:最初のケースが実際の状況である場合、HashMapが変更されないようにするためにCollections.unmodifiableMap()を使用することをお勧めします。 HashMapが指すオブジェクトも変更しないでください。そのため、finalキーワードを積極的に使用すると役立ちます。

そして、@ Lars Andrenが言うように、ほとんどの場合、ConcurrentHashMapが最良の選択です。

5
Denis Bazhenov

2つのスレッドからの適切な同期なしでHashMapを変更すると、簡単に競合状態になる可能性があります。

  • put()が内部テーブルのサイズ変更につながる場合、これには時間がかかり、他のスレッドは古いテーブルへの書き込みを続けます。
  • 異なるキーの2つのput()は、キーのハッシュコードがテーブルサイズを法として等しい場合、同じバケットの更新につながります。 (実際には、ハッシュコードとバケットインデックスの関係はより複雑ですが、衝突が発生する可能性があります。)
3