これはConcurrentHashMap
に関するJavaDocからの一節です。通常、取得操作はブロックされないため、更新操作と重複する可能性があります。これはget()
メソッドがスレッドセーフではないということですか?
「ただし、すべての操作はスレッドセーフですが、取得操作はロックを必要としません。また、すべてのアクセスを防ぐ方法でテーブル全体をロックすることはサポートされていません。このクラスは、スレッドの安全性。ただし、同期の詳細には影響しません。
通常、取得操作(getを含む)はブロックされないため、更新操作(putおよびremoveを含む)と重複する場合があります。取得には、開始時に保持されている最新の更新操作の結果が反映されます。」
get()
メソッドはスレッドセーフであり、他のユーザーはこの特定の問題に関して有用な回答を提供しました。
ただし、ConcurrentHashMap
はスレッドセーフドロップインの代わりにHashMap
に置き換わりますが、複数の操作を行う場合は、コードを大幅に変更する必要があります。たとえば、次のコードを使用します。
_if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);
_
マルチスレッド環境では、これは競合状態です。 ConcurrentHashMap.putIfAbsent(K key, V value)
を使用し、戻り値に注意する必要があります。戻り値は、put操作が成功したかどうかを示します。詳細についてはドキュメントをご覧ください。
これが競合状態である理由の明確化を求めるコメントへの回答。
同じキーを持つ2つの異なる値A
、B
がマップにそれぞれ_v1
_と_v2
_の2つの異なる値を配置する2つのスレッドがあるとします。キーは、最初はマップに存在しません。次のようにインターリーブします。
A
はcontainsKey
を呼び出し、キーが存在しないがすぐに中断されることを検出します。B
はcontainsKey
を呼び出して、キーが存在しないことを確認し、その値_v2
_を挿入する時間があります。A
は再開し、_v1
_を挿入し、スレッドput
によって挿入された値を「平和的に」上書きします(B
はスレッドセーフであるため)。スレッドB
は、独自の値_v2
_を正常に挿入したと「考える」が、マップには_v1
_が含まれている。スレッドB
がv2.updateSomething()
を呼び出し、マップのコンシューマー(他のスレッドなど)がそのオブジェクトにアクセスし、重要な更新を確認できるため、これは本当に災害です(「この訪問者のIPアドレスはDOSを実行しようとしているため、今後すべてのリクエストを拒否します」)。代わりに、オブジェクトはすぐにガベージコレクションされて失われます。
スレッドセーフです。ただし、スレッドセーフである方法は、期待どおりではない場合があります。あなたが見ることができるいくつかの「ヒント」があります:
このクラスは、スレッドの安全性に依存しているが同期の詳細には依存していないプログラムで
Hashtable
と完全に相互運用可能です。
より完全な図でストーリー全体を知るには、ConcurrentMap
インターフェースに注意する必要があります。
元のMap
は、非常に基本的な読み取り/更新メソッドを提供します。 Map
;のスレッドセーフな実装を作成することさえできました。私の同期メカニズムを考慮せずに人々が私のマップを使用できない場合がたくさんあります。これは典型的な例です:
_if (!threadSafeMap.containsKey(key)) {
threadSafeMap.put(key, value);
}
_
このコードは、マップ自体は安全ですが、スレッドセーフではありません。同時にcontainsKey()
を呼び出す2つのスレッドは、そのようなキーが存在しないと見なす可能性があるため、両方ともMap
に挿入します。
問題を解決するには、明示的に追加の同期を行う必要があります。同期されたキーワードによってマップのスレッドセーフが達成されると仮定すると、次の操作が必要になります。
_synchronized(threadSafeMap) {
if (!threadSafeMap.containsKey(key)) {
threadSafeMap.put(key, value);
}
}
_
このような追加コードでは、マップの「同期の詳細」について知る必要があります。上記の例では、同期が「同期」によって達成されることを知る必要があります。
ConcurrentMap
インターフェースは、これをさらに一歩進めます。これは、マップへの複数のアクセスを伴ういくつかの一般的な「複雑な」アクションを定義します。たとえば、上記の例はputIfAbsent()
として公開されています。これらの「複雑な」アクションにより、ConcurrentMap
のユーザー(ほとんどの場合)は、アクションをマップへの複数のアクセスと同期させる必要がありません。したがって、Mapの実装は、パフォーマンスを向上させるためにより複雑な同期メカニズムを実行できます。 ConcurrentHashhMap
は良い例です。実際に、スレッドの安全性は、マップの異なるパーティションに個別のロックを保持することにより維持されます。マップへの同時アクセスによって内部データ構造が破損したり、予期しない更新が失われたりすることがないため、スレッドセーフです。
上記のすべてを念頭に置いて、Javadocの意味がより明確になります。
ConcurrentHashMap
はスレッド同期のために「同期」を使用していないため、「取得操作(getを含む)は通常ブロックしません」。 get
のロジック自体がスレッドセーフを処理します。 Javadocをさらに見ると:
テーブルは、競合なしに指定された数の同時更新を許可しようとするために内部的にパーティション化されます
取得がブロックされないだけでなく、更新も同時に発生する可能性があります。ただし、非ブロッキング/同時更新は、スレッドが安全でないことを意味しません。単に、スレッドセーフのために単純な「同期」以外の方法を使用していることを意味します。
ただし、内部同期メカニズムは公開されていないため、ConcurrentMap
によって提供されるもの以外の複雑なアクションを実行する場合は、ロジックの変更を検討するか、ConcurrentHashMap
を使用しないことを検討する必要があります。 。例えば:
_// only remove if both key1 and key2 exists
if (map.containsKey(key1) && map.containsKey(key2)) {
map.remove(key1);
map.remove(key2);
}
_
ConcurrentHashmap.get()
は、次の意味でスレッドセーフです。
ConcurrentModificationException
などの例外はスローされません。Map
にも当てはまります。HashMap
は、hashCode
に基づいて "buckets" に分割されます。 ConcurrentHashMap
はこの事実を使用します。その同期メカニズムは、Map
全体ではなく、バケットのブロックに基づいています。この方法では、少数のスレッドが同時に少数の異なるバケットに書き込むことができます(一度に1つのスレッドが1つのバケットに書き込むことができます)。
ConcurrentHashMap
almostからの読み取りでは、同期は使用されません。キーの値を取得するときに、null
valueを検出するときに同期が使用されます。 ConcurrentHashMap
はnull
を値として保存できないので(はい、キーを除いて、値もnull
sにできません)、null
をフェッチすることをお勧めします。読み取りは、マップの初期化中に発生しましたentry(キーと値のペア)別のスレッド:キーが割り当てられたが、まだ値がなく、まだ保持されているときdefaultnull。
このような場合、読み取りスレッドは、エントリが完全に書き込まれるまで待機する必要があります。
したがって、read()
からの結果は、マップの現在の状態に基づきます。更新中のキーの値を読み取った場合、書き込みプロセスがまだ終了していないため、古い値を取得する可能性があります。
concurrentHashMapのget()は、揮発性の値を読み取るため、スレッドセーフです。また、値が任意のキーのnullの場合、get()メソッドはロックを取得するまで待機し、更新された値を読み取ります。
put()
メソッドがCHMを更新している場合、そのキーの値をnullに設定し、新しいエントリを作成してCHMを更新します。このnull値は、別のスレッドが同じキーでCHMを更新しているというシグナルとしてget()
メソッドによって使用されます。
これは、1つのスレッドが更新され、1つのスレッドが読み取りを行っているときに、ConcurrentHashMapメソッドを最初に呼び出したスレッドが時間内に最初に動作する保証がないことを意味します。
ボブがどこにいるかを伝えるアイテムの更新を考えてください。あるスレッドが、他のスレッドが更新するのとほぼ同時にボブがどこにいるかを尋ねると、リーダースレッドがボブのステータスを「内部」または「外部」として取得するかどうかを予測できません。更新スレッドが最初にメソッドを呼び出した場合でも、リーダースレッドは「外部」ステータスを取得する場合があります。
スレッドが互いに問題を引き起こすことはありません。コードはThreadSafeです。
1つのスレッドが無限ループに入ったり、奇妙なNullPointerExceptionsの生成を開始したり、古いステータスの半分と新しいステータスの半分で「内部」になったりすることはありません。