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


138

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

(現在、私は喜んで使っていjava.util.concurrent.ConcurrentHashMapて、パフォーマンスを向上させるために何を測定する必要がありませんが、シンプルであれば、単純に興味津々HashMap十分であろう。したがって、この質問がありません「?1は、私が使用すべき」またそれがパフォーマンスの問題です。むしろ、問題は「安全でしょうか?」)


4
ここでの多くの答えは、実行中のスレッドからの相互排除に関しては正しいですが、メモリー更新に関しては正しくありません。私はそれに応じて賛成/反対票を投じましたが、それでも肯定的な票を持つ多くの不正解があります。
ヒース国境

@Heath Borders、インスタンスaが静的に初期化された変更不可能なHashMapである場合、同時読み取りに対して安全である必要があります(更新がなかったため、他のスレッドは更新を見逃すことができなかったため)。
kaqqao 2016年

静的に初期化されていて、静的ブロックの外で変更されていない場合は、すべての静的初期化がによって同期されるため、問題ない可能性がありClassLoaderます。それ自体は別の質問に値します。それでも明示的に同期してプロファイルを作成し、実際のパフォーマンスの問題を引き起こしていることを確認します。
ヒースボーダーズ2016年

@HeathBorders-「メモリ更新」とはどういう意味ですか?JVMは、可視性、原子性、発生前の関係などを定義する正式なモデルですが、「メモリ更新」などの用語は使用しません。明確にする必要があります。できればJLSの用語を使用してください。
BeeOnRope 2017

2
@Dave-8年経ってもまだ回答を探していないと思いますが、記録としては、ほとんどすべての回答での主な混乱は、マップオブジェクトに対して行うアクションに焦点を合わせていることです。オブジェクトを変更しないことはすでに説明したので、それはすべて無関係です。唯一の潜在的な「落とし穴」は、説明していないへの参照どのように公開するかですMap。安全に行わないと安全ではありません。無事にやれば、そうです。私の答えの詳細。
BeeOnRope 2017

回答:


55

への参照が安全に公開されている場合に限り、イディオムは安全です安全な公開は、それ自体の内部に関連するものではなく、構築中のスレッドがマップへの参照を他のスレッドから見えるようにする方法を扱います。HashMapHashMap

基本的に、ここで可能な唯一の競合は、の構築と、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ため、読み取りスレッドは部分的に構築されたマップを自由に参照できることです。これはほとんど何でもでき、実際には、読み取りスレッドを無限ループに入れるようなことも行います

安全マップを公開するには、確立する必要がたまたま、前との関係を参照の書き込みHashMap(すなわち、出版物)およびその参照のその後の読者(すなわち、消費)。便利なことに、唯一のいくつかの覚えやすい方法があります達成それは、[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;

次に、すべてがコーシャです。null 以外の値を参照するリーダーは、ストアと必然的に関係が発生MAPし、マップの初期化に関連付けられているすべてのストアを参照します。

(2)(静的初期化子を使用)と(4)(finalを使用)はどちらもMAP実行時に動的に設定できないことを意味するため、他のメソッドはメソッドのセマンティクスを変更します。それを行う必要がない場合は、MAPとして宣言するだけで、static final HashMap<>安全な公開が保証されます。

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

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

  • あなたは既に宣言した瞬間に割り当てられることになるオブジェクトを作成することができ、Aを:ちょうど使用final(含むフィールドをstatic final静的メンバのため)。
  • 参照がすでに表示されている後、オブジェクトを後で割り当てたい場合は、揮発性フィールドbを使用します。

それでおしまい!

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

最後に、の使用にvolatileはいくつかの影響があります。多くのアーキテクチャ(x86など、特に読み取りで読み取りを渡すことができないアーキテクチャ)ではハードウェアバリアは必要ありませんが、コンパイル時に一部の最適化と並べ替えが行われない場合がありますが、これは効果は一般に小さいです。代わりに、実際に要求した以上のものが得られます-安全に公開できるだけでなく、同じ参照に必要なだけHashMap多くの未変更HashMapのを保存でき、すべての読者が安全に公開された地図を見ることができます。 。

詳細については、ShipilevまたはMansonとGoetzによるこのFAQを参照してください。


[1] Shipilevから直接引用。


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

bオプションで、synchronizedメソッドを使用して取得/設定、AtomicReferenceまたは何かを行うことができますが、ここでは、実行できる最低限の作業について説明します。

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


never modify HashMapstate of the map objectスレッドセーフであるとは限りません。公式文書にスレッドセーフであると記載されていない場合、神はライブラリの実装を知っています。
Jiang YD

@JiangYD-場合によっては、灰色の領域がある場合があります。「変更」と言うとき、実際に意味するのは、他のスレッドでの読み取りまたは書き込みと競合する可能性がある書き込みを内部で実行するアクションです。これらの書き込みは内部実装の詳細である可能性があるため、「読み取り専用」のように見える操作でさえ、get()実際にいくつかの書き込みを実行する可能性があります。たとえば、統計の更新(またはLinkedHashMapアクセス順の更新の場合)。したがって、適切に記述されたクラスは、次の場合にそれを明確にするドキュメントを提供する必要があります
BeeOnRope

...明らかに「読み取り専用」操作は、スレッドセーフの意味で、実際には内部的に読み取り専用です。たとえば、C ++標準ライブラリには、そのconstような意味でマークされたメンバー関数が本当に読み取り専用であるという包括的なルールがあります(内部的には、書き込みを実行する可能性がありますが、スレッドセーフにする必要があります)。constJavaにはキーワードがなく、文書化された包括的な保証については知りませんが、一般に標準ライブラリクラスは期待どおりに動作し、例外が文書化されています(LinkedHashMapRO操作などgetが明示的に安全でないと明示されている例を参照してください)。
BeeOnRope

@JiangYD-最後に、元の質問に戻ります。HashMap実際には、ドキュメントにこのクラスのスレッド安全動作があります。複数のスレッドが同時にハッシュマップにアクセスし、少なくとも1つのスレッドがマップを構造的に変更する場合、外部で同期する必要があります。(構造変更は、1つ以上のマッピングを追加または削除する操作です。インスタンスに既に含まれているキーに関連付けられた値を変更するだけでは、構造変更ではありません。)
BeeOnRope

したがって、HashMap読み取り専用であると予想されるメソッドは、構造的にを変更しないため、読み取り専用HashMapです。もちろん、この保証は他の任意のMap実装には当てはまらないかもしれませんが、問題はHashMap具体的にです。
BeeOnRope

70

Java Memory Modelの神であるJeremy Mansonは、このトピックについて3つのパートからなるブログを持っています-本質的に「不変のHashMapにアクセスするのは安全ですか」という質問をしているからです-その答えはイエスです。しかし、その質問に対する述語である「Is my HashMap is immutable」に答える必要があります。答えはあなたを驚かせるかもしれません-Javaには不変性を決定するための比較的複雑なルールのセットがあります。

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

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


3
これは良い点ですが、静的な初期化に依存しているため、参照がエスケープされないため、安全です。
Dave L.

5
これが非常に評価された回答(または回答)であるかどうかはわかりません。それは、一つには、質問に答えさえしないし、それが安全であるか否かを決定する一つの主要な原則である安全な出版については触れていない。「答え」は「だまされやすい」に要約され、ここに3つの(複雑な)リンクが表示されます。
BeeOnRope 2017

彼は最初の文の最後で質問に答えます。答えであるという点で、彼は不変性(質問の最初の段落で言及されている)は簡単ではないという点と、そのトピックをさらに説明する貴重なリソースを提起しています。ポイントは、それが答えであるかどうかを測定するのではなく、答えが他の人にとって「有用」であったかどうかを測定します。回答が受け入れられたことは、OPが探していた回答であり、回答を受け取ったことを意味します。
ジェシー

@Jesse彼は最初の文の終わりで質問に答えていません。彼は「不変オブジェクトにアクセスするのは安全ですか」という質問に答えています。これは、次の文で指摘するようにOPの質問に当てはまる場合と当てはまらない場合があります。基本的に、これはほぼリンクのみの「自分で考えてみる」タイプの回答であり、SOには適した回答ではありません。賛成票については、10.5歳であることと、頻繁に検索されるトピックの機能であると思います。過去数年でネットでの賛成票はごくわずかしか受け取っていないため、おそらく人々がやって来ます:)。
BeeOnRope

35

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

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

詳細については、新しいJavaメモリモデルに関するBrian Goetzの記事を参照してください。


ヒースの二重提出でごめんなさい、私が私の提出後に初めて私はあなたに気づきました。:)
Alexander

2
記憶効果を実際に理解している他の人がここにいるのはうれしいです。
ヒース国境

1
実際、オブジェクトが適切に初期化される前にオブジェクトを参照するスレッドはないので、この場合は問題ではないと思います。
Dave L.

1
それは、オブジェクトの初期化方法に完全に依存します。
ビルミシェル

1
HashMapがいったん初期化されると、彼はそれをさらに更新するつもりはないという質問です。その後、彼はそれを読み取り専用のデータ構造として使用したいだけです。彼のマップに保存されているデータが不変であれば、そうすることは安全だと思います。
Binita Bharati

9

もう少し調べた後、私はこれをjavaドキュメントで見つけました(強調は私のものです):

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

これは、そこにあるステートメントの逆が真実であると仮定すると、安全であることを暗示しているようです。


1
これは優れたアドバイスですが、他の回答が述べているように、不変で安全に公開されたマップインスタンスの場合には、より微妙な回答があります。しかし、あなたはあなたがあなたが何をしているのか知っている場合にのみそれをするべきです。
Alex Miller

1
うまくいけば、これらのような質問で、私たちの多くが私たちが何をしているのかを知ることができます。
Dave L.

これは本当に正しくありません。他の回答が示すように、最後の変更とその後のすべての「スレッドセーフ」読み取りの間には、前に発生が必要です。通常、これは、オブジェクトが作成され、その変更が行われた、オブジェクトを安全に公開する必要があることを意味します。マークされた最初の正解を参照してください。
markspace

9

ある状況下では、非同期のHashMapからのget()が無限ループを引き起こす可能性があることに注意してください。これは、同時にput()がマップの再ハッシュを引き起こす場合に発生する可能性があります。

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


1
実際、これはCPUを消費せずにJVMをハングアップさせるのを見ました(これはおそらくもっと悪いことです)
Peter Lawrey

2
私が考えるこのコードは、それが無限ループを得るためにはもはや不可能だというように書き直されていません。ただし、他の理由で、非同期のHashMapからの取得や書き込みを実行してはいけません。
Alex Miller

@AlexMillerは、他の理由(安全な公開について言及していると思います)は別として、ドキュメントで明示的に許可されていない限り、実装の変更がアクセス制限を緩和する理由になるとは思いません。偶然にも、HashMapのJavadocを Javaの8のためには、まだこの警告が含まれています:Note that this implementation is not synchronized. If multiple threads access a hash map concurrently, and at least one of the threads modifies the map structurally, it must be synchronized externally.
shmosel

8

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


これにアクセスするスレッドがロックを取得するかどうかはわかりませんが、オブジェクトが初期化されるまでオブジェクトへの参照を取得できないため、古いコピーが存在することはないと思います。
Dave L.

@Alex:HashMapへの参照は、同じメモリの可視性を保証するために揮発性にすることができます。@Dave:そのctor の作業がスレッドから見えるようになる前に、新しいobj への参照を確認すること可能です。
Chris Vest

@クリスチャン一般的なケースでは、確かに。このコードではそうではないと言っていました。
Dave L.

ランダムロックを取得しても、スレッドのCPUキャッシュ全体がクリアされることは保証されません。これは、JVMの実装に依存しており、この方法で行われることはほとんどありません。
ピエール

私はピエールに同意します。ロックを取得するだけでは十分ではないと思います。変更を表示するには、同じロックで同期する必要があります。
damluar 2016年

5

http://www.ibm.com/developerworks/java/library/j-jtp03304/によると#初期化の安全性はHashMapを最終フィールドにすることができ、コンストラクターが完了すると安全に公開されます。

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


この回答は質が低く、@ taylor gauthierからの回答と同じですが、詳細が少なくなっています。
Snicolas 2016年

1
うーん...お尻にはならないが、あなたはそれを逆に持っている。テイラーは「いいえ、このブログ投稿を見てください、答えはあなたを驚かせるかもしれません」と言ったのに対し、この答えは実際に私が知らなかった新しい何かを追加します...コンストラクタ。この答えは素晴らしいです、そして私はそれを読んでうれしいです。
Ajax

えっ?これは、より高い評価の回答をスクロールした後に見つけた唯一の正解です。キーは安全に公開されており、これはそれについて言及する唯一の回答です。
BeeOnRope 2017

3

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

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


2
これは不変性を強制するという点で「安全」ですが、スレッドセーフの問題には対応していません。UnmodifiableMapラッパーを使用してマップに安全にアクセスできる場合は、マップがなくても安全であり、逆の場合も同様です。
Dave L.

2

この質問は、Brian Goetzの「Java Concurrency in Practice」の本(リスト16.8、ページ350)で扱われています。

@ThreadSafe
public class SafeStates {
    private final Map<String, String> states;

    public SafeStates() {
        states = new HashMap<String, String>();
        states.put("alaska", "AK");
        states.put("alabama", "AL");
        ...
        states.put("wyoming", "WY");
    }

    public String getAbbreviation(String s) {
        return states.get(s);
    }
}

以来statesとして宣言されfinal、その初期化が、所有者のクラスのコンストラクタ内で達成された後に、このマップを読み込み、どのスレッドが他のスレッドを提供しないコンストラクタが終了すると、マップの内容を変更しようとする時のようにそれを見ることが保証されます。


1

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

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

とはいえ、HashMapからの同時読み取りが安全である場合を除いて、何もしません。

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

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

私の意図は、Javaメモリモデルに関するより詳細な回答と矛盾しないことです。この回答は、同時実行の問題は別として、ConcurrentHashMapとHashMapの間には少なくとも1つのAPIの違いがあることを指摘することを目的としています。


警告をありがとうございますが、nullキーまたは値を使用する試みはありません。
Dave L.

ないだろうと思った。コレクションのnullはJavaのクレイジーコーナーです。
スティーブジェソップ

私はこの答えに同意しません。「HashMapからの同時読み取りは安全です」自体は正しくありません。読み取りが変更可能であるか不変であるマップに対して行われているかは示されません。正しければ、「不変のHashMapからの同時読み取りは安全です」を読む必要があります
Taylor Gautier

2
あなた自身がリンクしている記事によらない:要件は、マップを変更してはならない(かつ以前の変更がすべてのリーダースレッドから見える必要がある)ことであり、不変である(Javaの技術用語であり、安全のために十分ですが、必要条件ではありません)。
スティーブジェソップ

また、注意事項...クラスの初期化は同じロックで暗黙的に同期します(そうです、静的フィールド初期化子でデッドロックする可能性があります)。したがって、初期化が静的に行われると、初期化が完了する前に他の誰もそれを見ることができません。取得した同じロックのClassLoader.loadClassメソッドでブロックする必要があります...そして、同じフィールドの異なるコピーを持つ異なるクラスローダーについて疑問に思っている場合、あなたは正しいでしょう...しかし、それは競合状態の概念。クラスローダーの静的フィールドは、メモリフェンスを共有します。
Ajax

0

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

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

つまり、マルチスレッドの状況でHashMapから読み取ることは問題ありませんが、複数の書き込みがあった場合は必ずConcurrentHashMapを使用します。

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


2
たとえば、一時的なインスタンス変数を内部的に使用しているために、単純に同時に読み取ると問題が発生する可能性があるクラスがあると思います。そのため、ロック/ミューテックスコードのクイックスキャン以上に、ソースを注意深く調べる必要があるでしょう。
デイブL.

0

初期化とすべての書き込みが同期されている場合は、保存されます。

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

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

}

揮発性の書き込みが同期を処理するため、次のコードは保存されます。

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の場合にも機能します。メソッドがコンストラクタの場合。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.