1. ホーム
  2. java

[解決済み] 複数のスレッドから java.util.HashMap の値を取得することは安全ですか(変更なし)?

2022-05-07 03:54:53

質問

マップを構築し、一度初期化したら二度と変更しないケースがあります。 しかし、複数のスレッドから(get(key)のみで)アクセスされることになる。 このような場合 java.util.HashMap をこのような形で使用することは可能でしょうか?

(現在、私は嬉々として java.util.concurrent.ConcurrentHashMap しかし、単純な HashMap で十分だと思います。 したがって、この質問は ではない どれを使えばいいのか、性能の問題でもありません。 むしろ問題は、"安全でしょうか?")です。

どのように解決するのですか?

あなたの慣用句は安全です もし への参照は HashMap 無事公開 . の内部に関することよりも、むしろ 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 と、その後に別のスレッドで読み込まれるため、読み込むスレッドは、部分的に構築されたマップを自由に見ることができます。これによって、かなり多くのことが可能になります なんでも のようなことができ、実際にも 読み込みスレッドを無限ループに陥れる .

安全にマップを公開するためには 起こる前 という関係があります。 リファレンスの記述 を使用します。 HashMap (という 出版物 )と、その参照先の読者(すなわち消費者)の2つからなる。便利なことに、覚えやすい方法はいくつかあるだけです。 達成する その [1] :

  1. 適切にロックされたフィールドを介して、参照を交換する ( JLS 17.4.5 )
  2. 静的イニシャライザーを使って、ストアの初期化を行う ( JLS 12.4 )
  3. volatileフィールドを介して参照を交換する ( 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) (静的イニタライザーを使用) の両方が、メソッドのセマンティクスを変更するためです。 ファイナル を設定することができないことを意味します。 MAP 実行時に動的に もし 必要 を宣言してください。 MAP として static final HashMap<> で、安全な出版が保証されます。

実際には、"never-modified object"に安全にアクセスするためのルールは単純です。

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

  • 宣言の時点ですでに割り当てられるオブジェクトを作成することができます a を使うだけです。 final フィールドを含む static final 静的メンバの場合)。
  • 参照が既に可視化された後に、後でオブジェクトを割り当てたい場合: volatileフィールドを使用します。 b .

以上です。

実際に使ってみると、非常に効率的です。を使用することで static final フィールドは、例えば、JVMがプログラムの寿命まで値を変更しないと仮定して、大きく最適化することができます。を使うことで final メンバーフィールドを使用すると 最も アーキテクチャでは、通常のフィールド読み出しと同等の方法でフィールドを読み出すことができ、さらなる最適化を阻害することはありません。 c .

最後に volatile 多くのアーキテクチャ(x86など、特に読み込みが読み込みを通過することを許さないアーキテクチャ)ではハードウェアバリアは必要ありませんが、コンパイル時にいくつかの最適化と並べ替えが発生しないかもしれません - しかしこの影響は一般的に小さいです。それと引き換えに、あなたは実際に要求した以上のものを手に入れることができます。 HashMap を保存することができ、さらに多くの修正されていない HashMap を同じ参照先に設定すれば、すべての読者が安全に発行されたマップを見ることができることを保証します。

より詳細な情報は、以下を参照してください。 シップイレフ または マンソンとゲッツによるこのFAQ .


[1】直接の引用元 シピレフ .


a 複雑そうに聞こえますが、私が言いたいのは、コンストラクション時に参照を割り当てることができるということです。宣言時点、コンストラクタ(メンバーフィールド)または静的イニシャライザ(静的フィールド)のいずれかにおいてです。

b オプションで synchronized メソッドで取得/設定するか AtomicReference などと言われそうですが、最低限の作業で済むという話です。

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