web-dev-qa-db-ja.com

Java.util.HashMapから複数のスレッドから値を取得するのは安全ですか(変更なし)?

マップが作成される場合があり、初期化されると、再び変更されることはありません。ただし、複数のスレッドから(get(key)のみを介して)アクセスされます。このようにJava.util.HashMapを使用しても安全ですか?

(現在、私は喜んでJava.util.concurrent.ConcurrentHashMapを使用しており、パフォーマンスを改善する必要性は測定されていませんが、単純なHashMapで十分な場合は単純に興味があります。したがって、この質問は「どちらを使用すればよいですか?」またはパフォーマンスの問題ではありません。むしろ、問題は「安全ですか?」です)

132
Dave L.

あなたのイディオムは安全ですifHashMapへの参照がsafely publishedである場合に限ります。 safe publicationは、HashMap自体の内部に関連するものではなく、構築スレッドが他のスレッドからマップへの参照を可視化する方法を扱います。

基本的に、ここで唯一可能な競合は、HashMapの構築と、完全に構築される前にアクセスする可能性のある読み取りスレッドとの間の競合です。議論のほとんどはマップオブジェクトの状態に何が起こるかについてですが、これを変更することはないので、これは無関係です。したがって、唯一の興味深い部分はHashMap参照が公開される方法です。

たとえば、次のような地図を公開するとします。

_class SomeClass {
   public static HashMap<Object, Object> MAP;

   public synchronized static setMap(HashMap<Object, Object> m) {
     MAP = m;
   }
}
_

...ある時点でsetMap()がマップで呼び出され、他のスレッドは_SomeClass.MAP_を使用してマップにアクセスし、次のようにnullをチェックします。

_HashMap<Object,Object> map = SomeClass.MAP;
if (map != null) {
  .. use the map
} else {
  .. some default behavior
}
_

おそらくあたかもそうであるように見えても、これは安全ではありません。問題は、_SomeObject.MAP_のセットと別のスレッドでの後続の読み取りとの間に happens-before 関係がないため、読み取りスレッドが自由に参照できることです。部分的に構築されたマップ。これはほとんどanythingを行うことができ、実際には 読み取りスレッドを無限ループに入れる のようなことをします。

マップを安全に公開するには、happens-before参照の書き込みHashMap(つまりpublication)およびその参照の後続の読者(つまり、消費)。便利なことに、覚えやすい簡単な方法は accomplish[1]

  1. 適切にロックされたフィールドを介して参照を交換します( JLS 17.4.5
  2. 静的イニシャライザを使用してストアの初期化を行います( JLS 12.4
  3. 揮発性フィールド( JLS 17.4.5 )を介して、またはこのルールの結果として、AtomicXクラスを介して参照を交換します
  4. 最終フィールドに値を初期化します( JLS 17.5 )。

シナリオで最も興味深いのは、(2)、(3)、および(4)です。特に、(3)は上記のコードに直接適用されます。MAPの宣言を次のように変換する場合:

_public static volatile HashMap<Object, Object> MAP;
_

すべてがコーシャです:non-nullの値を見る読者は、必然的にhappens-beforeのストアとMAPの関係があり、したがって、関連付けられているすべてのストアを見るマップの初期化。

(2)(静的イニシャライザーを使用)と(4)(finalを使用)の両方が実行時にMAPを動的に設定できないことを意味するため、他のメソッドはメソッドのセマンティクスを変更します。 needを実行しない場合は、MAPを_static final HashMap<>_として宣言するだけで、安全な公開が保証されます。

実際には、ルールは「変更されていないオブジェクト」に安全にアクセスするために単純です。

本質的に不変ではないオブジェクトを公開している場合(すべてのフィールドでfinalと宣言されている場合)、および:

  • 宣言の時点で割り当てられるオブジェクトをすでに作成できますafinalフィールドを使用するだけです(静的メンバーの_static final_を含む)。
  • 参照が既に表示された後、後でオブジェクトを割り当てたい場合:volatileフィールドを使用しますb

それでおしまい!

実際には、非常に効率的です。たとえば、_static final_フィールドを使用すると、JVMはプログラムの存続期間中に値が変更されていないと想定し、大幅に最適化できます。 finalメンバーフィールドを使用すると、mostアーキテクチャが通常のフィールド読み取りと同等の方法でフィールドを読み取ることができ、さらなる最適化を妨げません。c

最後に、volatileの使用にはある程度の影響があります。多くのアーキテクチャ(特にx86など、読み取りで読み取りを許可しないアーキテクチャ)にはハードウェアバリアは必要ありませんが、コンパイル時間-しかし、この効果は一般的に小さいです。それと引き換えに、あなたは実際にあなたが要求したもの以上のものを手に入れます-あなたは安全に1つのHashMapを発行できるだけでなく、あなたは同じ参照にあなたが望むだけ多くの未変更のHashMapsを保存できますすべての読者に安全に公開された地図が表示されること。

詳細については、 Shipilev または this FAQ by Manson and Goetz を参照してください。


[1] shipilev から直接引用。


a それは複雑に聞こえますが、私が意味するのは、宣言時に、またはコンストラクター(メンバーフィールド)または静的初期化子(静的フィールド)のいずれかで、構築時に参照を割り当てることができるということです。

b 必要に応じて、synchronizedメソッドを使用して取得/設定、またはAtomicReferenceまたは何かを使用できますが、できる最小限の作業について説明しています。

c非常に弱いメモリモデル(yo、Alphaを見ている)の一部のアーキテクチャでは、final読み取りの前に何らかのタイプの読み取りバリアが必要な場合がありますが、今日では非常にまれです。

42
BeeOnRope

Java Memory Modelに関しては神であるJeremy Mansonは、このトピックに関する3つのパートのブログを持っています-本質的に、「不変のHashMapにアクセスしても安全ですか?」 -それに対する答えはイエスです。しかし、あなたはその質問に対する述語に答えなければなりません-「私のHashMapは不変です」。答えはあなたを驚かせるかもしれません-Java不変性を決定します。

このトピックの詳細については、ジェレミーのブログ投稿をご覧ください。

Javaでの不変性に関するパート1 http://jeremymanson.blogspot.com/2008/04/immutability-in-Java.html

Javaの不変性に関するパート2: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-2.html

Javaの不変性に関するパート3: http://jeremymanson.blogspot.com/2008/07/immutability-in-Java-part-3.html

70
Taylor Gautier

読み取りは同期の観点からは安全ですが、メモリの観点からは安全ではありません。これはJava開発者の間で広く誤解されているものです。 この答え の評価を確認してください。)

他のスレッドを実行している場合、現在のスレッドからのメモリ書き込みがない場合、HashMapの更新されたコピーが表示されないことがあります。メモリ書き込みは、synchronizedまたはvolatileキーワードを使用するか、いくつかのJava同時実行構文を使用して行われます。

詳細については、 Brian Goetzの新しいJava Memory Model に関する記事を参照してください。

35
Heath Borders

もう少し見てみると、私はこれを Java doc (強調マイン)で見つけました:

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

これは、声明の逆が真実であると仮定して、それが安全であることを暗示しているようです。

9
Dave L.

1つの注意点は、状況によっては、非同期HashMapからのget()が無限ループを引き起こす可能性があることです。これは、並行put()がマップの再ハッシュを引き起こす場合に発生する可能性があります。

http://lightbody.net/blog/2005/07/hashmapget_can_cause_an_infini.html

9
Alex Miller

しかし、重要なねじれがあります。マップにアクセスするのは安全ですが、一般に、すべてのスレッドがHashMapのまったく同じ状態(したがって値)を見ることが保証されるわけではありません。これは、1つのスレッド(たとえば、それを実装したスレッド)によって行われたHashMapへの変更がそのCPUのキャッシュに存在し、メモリフェンス操作が実行されるまで他のCPUで実行されているスレッドによって認識されないマルチプロセッサシステムで発生する可能性がありますキャッシュの一貫性を確保するために実行されます。 Java言語仕様はこれについて明示的です。解決策は、メモリフェンス操作を発行するロック(同期(...))を取得することです。 HashMapは各スレッドに任意のロックを取得し、その時点から、HashMapが再度変更されるまで、任意のスレッドからHashMapにアクセスできます。

8
Alexander

http://www.ibm.com/developerworks/Java/library/j-jtp03304/ #HashMapを最終フィールドにし、コンストラクターが終了すると安全に公開できるように初期化の安全性に従って。

...新しいメモリモデルでは、コンストラクターの最終フィールドの書き込みと、別のスレッドのそのオブジェクトへの共有参照の初期ロードとの間に発生する関係に似たものがあります。 ...

5
bodrin

したがって、あなたが説明したシナリオは、大量のデータをマップに配置する必要があり、データの設定が完了したら、それを不変として扱います。 「安全」な方法の1つ(実際に不変として扱われることを強制していることを意味します)は、不変にする準備ができたら参照をCollections.unmodifiableMap(originalMap)に置き換えることです。

同時に使用した場合にマップがどれほどひどく失敗するかの例、および私が言及した推奨される回避策については、次のバグパレードエントリを確認してください。 bug_id = 6423457

3
Will

シングルスレッドコードであっても、ConcurrentHashMapをHashMapに置き換えることは安全ではない可能性があることに注意してください。 ConcurrentHashMapは、キーまたは値としてnullを禁止します。 HashMapはそれらを禁止していません(尋ねないでください)。

そのため、セットアップ中に既存のコードがコレクションにnullを追加する可能性が低い状況(おそらく何らかの障害の場合)で、説明されているようにコレクションを置き換えると、機能的な動作が変更されます。

とはいえ、HashMapからの同時読み取りが安全でない限り、他に何もしなければ。

[編集:「同時読み取り」により、同時変更も行われないことを意味します。

他の答えは、これを確実にする方法を説明します。 1つの方法は、マップを不変にすることですが、必要ではありません。たとえば、JSR133メモリモデルは、スレッドの開始を同期アクションとして明示的に定義します。つまり、スレッドBを開始する前にスレッドAで行った変更は、スレッドBで表示されます。

私の意図は、Java Memory Model。 、1つを別のものに置き換えたシングルスレッドプログラムでさえも回収できます。]

1
Steve Jessop

初期化とすべてのputが同期される場合、保存されます。

クラスローダーが同期を処理するため、次のコードは保存されます。

public static final HashMap<String, String> map = new HashMap<>();
static {
  map.put("A","A");

}

Volatileを記述すると同期が処理されるため、次のコードは保存されます。

class Foo {
  volatile HashMap<String, String> map;
  public void init() {
    final HashMap<String, String> tmp = new HashMap<>();
    tmp.put("A","A");
    // writing to volatile has to be after the modification of the map
    this.map = tmp;
  }
}

Finalも揮発性なので、メンバー変数がfinalの場合にも機能します。そして、メソッドがコンストラクターである場合。

0
TomWolk

http://www.docjar.com/html/api/Java/util/HashMap.Java.html

これがHashMapのソースです。おわかりのように、ロック/ミューテックスコードはまったくありません。

これは、マルチスレッドの状況でHashMapから読み取ることは問題ないが、複数の書き込みがあった場合は間違いなくConcurrentHashMapを使用することを意味します。

興味深いのは、.NET HashTableとDictionary <K、V>の両方に同期コードが組み込まれていることです。

0
FlySwat