web-dev-qa-db-ja.com

Java synchronizedブロックとCollections.synchronizedMap

次のコードは、synchronizedMapの呼び出しを正しく同期するように設定されていますか?

public class MyClass {
  private static Map<String, List<String>> synchronizedMap = Collections.synchronizedMap(new HashMap<String, List<String>>());

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      //do something with values
    }
  }

  public static void addToMap(String key, String value) {
    synchronized (synchronizedMap) {
      if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
      }
      else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
      }
    }
  }
}

私の理解では、addToMap()の呼び出しを完了する前に別のスレッドがremove()またはcontainsKey()を呼び出すことを防ぐために、put()の同期ブロックが必要です。元々doWork()でマップを作成したため、addToMap()が戻る前に別のスレッドがremove()の同期ブロックに入ることができないため、Collections.synchronizedMap()の同期ブロックは必要ありません。あれは正しいですか?これを行うためのより良い方法はありますか?

81
Ryan Ahearn

Collections.synchronizedMap()は、マップで実行する各アトミック操作が同期されることを保証します。

ただし、マップ上で2つ(またはそれ以上)の操作を実行するには、ブロック内で同期する必要があります。そうです-あなたは正しく同期しています。

87
Yuval Adam

JDK 6を使用している場合は、 ConcurrentHashMap をチェックアウトすることをお勧めします。

そのクラスのputIfAbsentメソッドに注意してください。

14
TofuBeer

コードにわずかなバグがある場合、potentialがあります。

[PDATE:彼はmap.remove()を使用しているため、この説明は完全に有効ではありません。初めてその事実を見逃した。 :(それを指摘してくれた質問の著者に感謝します。残りはそのままにしておきますが、リードステートメントを変更してpotentiallyaバグ。]

doWork()では、スレッドセーフな方法でマップからList値を取得します。ただし、その後は、安全でない問題でそのリストにアクセスします。たとえば、あるスレッドがdoWork()のリストを使用している間に、別のスレッドがsynchronizedMap.get(key).add(value) in addToMap( )。これらの2つのアクセスは同期されません。経験則では、コレクションのスレッドセーフな保証は、それらが保存するキーまたは値には適用されません。

これを修正するには、次のような同期リストをマップに挿入します

List<String> valuesList = new ArrayList<String>();
valuesList.add(value);
synchronizedMap.put(key, Collections.synchronizedList(valuesList)); // sync'd list

または、doWork()のリストにアクセスしながら、マップ上で同期することもできます。

  public void doWork(String key) {
    List<String> values = null;
    while ((values = synchronizedMap.remove(key)) != null) {
      synchronized (synchronizedMap) {
          //do something with values
      }
    }
  }

最後のオプションは同時実行性を少し制限しますが、IMOの方が多少明確です。

また、ConcurrentHashMapに関する簡単なメモ。これは本当に便利なクラスですが、常に同期HashMapの適切な代替とは限りません。そのJavadocsから引用して、

このクラスは、スレッドの安全性に依存するが、同期の詳細に依存しないプログラムでは、Hashtableと完全に相互運用可能です。

言い換えると、putIfAbsent()はアトミック挿入に最適ですが、その呼び出し中にマップの他の部分が変更されないことを保証しません。原子性のみを保証します。サンプルプログラムでは、put()以外のことについて(同期された)HashMapの同期の詳細に依存しています。

最後のもの。 :)Java Concurrency in Practiceからのこの素晴らしい引用は、マルチスレッドプログラムのデバッグの設計に常に役立ちます。

複数のスレッドがアクセスできる可変状態変数ごとに、その変数へのすべてのアクセスは、同じロックを保持して実行する必要があります。

13
JLR

はい、正しく同期しています。これについて詳しく説明します。 synchronizedMapオブジェクトのメソッド呼び出しのシーケンスで、後続のメソッド呼び出しで以前のメソッド呼び出しの結果に依存する必要がある場合にのみ、synchronizedMapオブジェクトの2つ以上のメソッド呼び出しを同期する必要があります。このコードを見てみましょう:

_synchronized (synchronizedMap) {
    if (synchronizedMap.containsKey(key)) {
        synchronizedMap.get(key).add(value);
    }
    else {
        List<String> valuesList = new ArrayList<String>();
        valuesList.add(value);
        synchronizedMap.put(key, valuesList);
    }
}
_

このコードでは

_synchronizedMap.get(key).add(value);
_

そして

_synchronizedMap.put(key, valuesList);
_

メソッド呼び出しは前の結果に依存します

_synchronizedMap.containsKey(key)
_

メソッド呼び出し。

メソッド呼び出しのシーケンスが同期されていない場合、結果が間違っている可能性があります。たとえば、_thread 1_はメソッドaddToMap()を実行しており、_thread 2_はメソッドdoWork()を実行していますsynchronizedMapオブジェクトに対するメソッド呼び出しのシーケンスは、次のようになります:_Thread 1_はメソッドを実行しました

_synchronizedMap.containsKey(key)
_

結果は「true」です。そのオペレーティングシステムが実行制御を_thread 2_に切り替えて実行した後

_synchronizedMap.remove(key)
_

その実行制御が_thread 1_に戻され、たとえば実行された後

_synchronizedMap.get(key).add(value);
_

synchronizedMapオブジェクトにはkeyが含まれていると考えられ、synchronizedMap.get(key)NullPointerExceptionを返すため、nullがスローされます。 synchronizedMapオブジェクトに対するメソッド呼び出しのシーケンスが互いの結果に依存していない場合、シーケンスを同期する必要はありません。たとえば、このシーケンスを同期する必要はありません。

_synchronizedMap.put(key1, valuesList1);
synchronizedMap.put(key2, valuesList2);
_

ここに

_synchronizedMap.put(key2, valuesList2);
_

メソッド呼び出しは前の結果に依存しません

_synchronizedMap.put(key1, valuesList1);
_

メソッド呼び出し(スレッドが2つのメソッド呼び出しの間に干渉したかどうかは関係なく、たとえば_key1_を削除しました)。

10
Sergey

それは私には正しいようです。何かを変更する場合は、Collections.synchronizedMap()の使用を停止し、明確にするためにすべてを同じ方法で同期します。

また、私は交換します

  if (synchronizedMap.containsKey(key)) {
    synchronizedMap.get(key).add(value);
  }
  else {
    List<String> valuesList = new ArrayList<String>();
    valuesList.add(value);
    synchronizedMap.put(key, valuesList);
  }

List<String> valuesList = synchronziedMap.get(key);
if (valuesList == null)
{
  valuesList = new ArrayList<String>();
  synchronziedMap.put(key, valuesList);
}
valuesList.add(value);
4
Paul Tomblin

Googleコレクション 'Multimapをご覧ください。 このプレゼンテーション の28ページ。

何らかの理由でそのライブラリを使用できない場合は、ConcurrentHashMapの代わりにSynchronizedHashMapを使用することを検討してください。要素リストがまだ存在しない場合にアトミックに追加できる、気の利いたputIfAbsent(K,V)メソッドがあります。また、使用パターンでそうする必要がある場合は、マップ値にCopyOnWriteArrayListを使用することを検討してください。

2
Barend

同期方法は正しいです。しかし、キャッチがあります

  1. Collectionフレームワークによって提供される同期ラッパーは、メソッド呼び出しI.e add/get/containsが相互に排他的に実行されることを保証します。

ただし、実際には、通常、値を入力する前にマップを照会します。したがって、2つの操作を行う必要があるため、同期ブロックが必要です。したがって、使用方法は正しいです。しかしながら。

  1. Collectionフレームワークで利用可能なMapの同時実装を使用することもできます。 「ConcurrentHashMap」の利点は

a。 APIに「putIfAbsent」があり、同じことをより効率的に行います。

b。効率的:dCocurrentMapはキーをロックするだけなので、マップの世界全体をブロックしません。キーと値をブロックした場所。

c。あなたのマップオブジェクトの参照をコードベースのどこか他の場所に渡し、あなたや他の開発者が間違って使用する可能性があります。つまり、彼はマップのオブジェクトをロックせずにすべてadd()またはget()するだけです。したがって、彼の呼び出しは、同期ブロックに対して相互に排他的に実行されません。ただし、同時実装を使用すると、誤って使用/実装されることはないという安心感が得られます。

2
Jai Pandit