web-dev-qa-db-ja.com

ConcurrentHashMap値の反復処理はスレッドセーフですか?

ConcurrentHashMap のjavadocでは、次のとおりです。

通常、取得操作(getを含む)はブロックされないため、更新操作(putおよびremoveを含む)と重複する場合があります。取得には、開始時に保持されている最後に完了した更新操作の結果が反映されます。 putAllやclearなどの集約操作の場合、同時取得は一部のエントリのみの挿入または削除を反映する場合があります。同様に、イテレータと列挙は、イテレータ/列挙の作成時またはそれ以降のある時点でのハッシュテーブルの状態を反映する要素を返します。 ConcurrentModificationExceptionはスローされません。 ただし、反復子は一度に1つのスレッドのみが使用するように設計されています。

どういう意味ですか? 2つのスレッドで同時にマップを反復しようとするとどうなりますか?反復中にマップに値を追加または削除するとどうなりますか?

136
Palo

どういう意味ですか?

つまり、ConcurrentHashMapから取得する各イテレータは、単一のスレッドで使用するように設計されており、渡すべきではありません。これには、for-eachループが提供する構文糖が含まれます。

2つのスレッドで同時にマップを反復しようとするとどうなりますか?

各スレッドが独自のイテレーターを使用する場合、期待どおりに機能します。

反復中にマップに値を追加または削除するとどうなりますか?

これを実行しても問題が発生しないことが保証されています(ConcurrentHashMapの「並行」が意味することの一部です)。ただし、一方のスレッドが他方のスレッドが実行するマップの変更を(マップから新しいイテレーターを取得せずに)見るという保証はありません。イテレータは、作成時のマップの状態を反映することが保証されています。さらなる変更がイテレータに反映される場合がありますが、そうである必要はありません。

結論として、次のような声明

for (Object o : someConcurrentHashMap.entrySet()) {
    // ...
}

あなたがそれを見るたびに大丈夫です(または少なくとも安全です)。

172
Waldheinz

このクラスを使用して、アクセスする2つのスレッドとConcurrentHashMapの共有インスタンスを変更する1つのスレッドをテストできます。

import Java.util.Map;
import Java.util.Random;
import Java.util.UUID;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;

public class ConcurrentMapIteration
{
  private final Map<String, String> map = new ConcurrentHashMap<String, String>();

  private final static int MAP_SIZE = 100000;

  public static void main(String[] args)
  {
    new ConcurrentMapIteration().run();
  }

  public ConcurrentMapIteration()
  {
    for (int i = 0; i < MAP_SIZE; i++)
    {
      map.put("key" + i, UUID.randomUUID().toString());
    }
  }

  private final ExecutorService executor = Executors.newCachedThreadPool();

  private final class Accessor implements Runnable
  {
    private final Map<String, String> map;

    public Accessor(Map<String, String> map)
    {
      this.map = map;
    }

    @Override
    public void run()
    {
      for (Map.Entry<String, String> entry : this.map.entrySet())
      {
        System.out.println(
            Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']'
        );
      }
    }
  }

  private final class Mutator implements Runnable
  {

    private final Map<String, String> map;
    private final Random random = new Random();

    public Mutator(Map<String, String> map)
    {
      this.map = map;
    }

    @Override
    public void run()
    {
      for (int i = 0; i < 100; i++)
      {
        this.map.remove("key" + random.nextInt(MAP_SIZE));
        this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
        System.out.println(Thread.currentThread().getName() + ": " + i);
      }
    }
  }

  private void run()
  {
    Accessor a1 = new Accessor(this.map);
    Accessor a2 = new Accessor(this.map);
    Mutator m = new Mutator(this.map);

    executor.execute(a1);
    executor.execute(m);
    executor.execute(a2);
  }
}

例外はスローされません。

アクセサスレッド間で同じイテレータを共有すると、デッドロックが発生する可能性があります。

import Java.util.Iterator;
import Java.util.Map;
import Java.util.Random;
import Java.util.UUID;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;

public class ConcurrentMapIteration
{
  private final Map<String, String> map = new ConcurrentHashMap<String, String>();
  private final Iterator<Map.Entry<String, String>> iterator;

  private final static int MAP_SIZE = 100000;

  public static void main(String[] args)
  {
    new ConcurrentMapIteration().run();
  }

  public ConcurrentMapIteration()
  {
    for (int i = 0; i < MAP_SIZE; i++)
    {
      map.put("key" + i, UUID.randomUUID().toString());
    }
    this.iterator = this.map.entrySet().iterator();
  }

  private final ExecutorService executor = Executors.newCachedThreadPool();

  private final class Accessor implements Runnable
  {
    private final Iterator<Map.Entry<String, String>> iterator;

    public Accessor(Iterator<Map.Entry<String, String>> iterator)
    {
      this.iterator = iterator;
    }

    @Override
    public void run()
    {
      while(iterator.hasNext()) {
        Map.Entry<String, String> entry = iterator.next();
        try
        {
          String st = Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
        } catch (Exception e)
        {
          e.printStackTrace();
        }

      }
    }
  }

  private final class Mutator implements Runnable
  {

    private final Map<String, String> map;
    private final Random random = new Random();

    public Mutator(Map<String, String> map)
    {
      this.map = map;
    }

    @Override
    public void run()
    {
      for (int i = 0; i < 100; i++)
      {
        this.map.remove("key" + random.nextInt(MAP_SIZE));
        this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
      }
    }
  }

  private void run()
  {
    Accessor a1 = new Accessor(this.iterator);
    Accessor a2 = new Accessor(this.iterator);
    Mutator m = new Mutator(this.map);

    executor.execute(a1);
    executor.execute(m);
    executor.execute(a2);
  }
}

アクセサースレッドとミューテータースレッド間で同じIterator<Map.Entry<String, String>>の共有を開始すると、Java.lang.IllegalStateExceptionsがポップアップし始めます。

import Java.util.Iterator;
import Java.util.Map;
import Java.util.Random;
import Java.util.UUID;
import Java.util.concurrent.ConcurrentHashMap;
import Java.util.concurrent.ExecutorService;
import Java.util.concurrent.Executors;

public class ConcurrentMapIteration
{
  private final Map<String, String> map = new ConcurrentHashMap<String, String>();
  private final Iterator<Map.Entry<String, String>> iterator;

  private final static int MAP_SIZE = 100000;

  public static void main(String[] args)
  {
    new ConcurrentMapIteration().run();
  }

  public ConcurrentMapIteration()
  {
    for (int i = 0; i < MAP_SIZE; i++)
    {
      map.put("key" + i, UUID.randomUUID().toString());
    }
    this.iterator = this.map.entrySet().iterator();
  }

  private final ExecutorService executor = Executors.newCachedThreadPool();

  private final class Accessor implements Runnable
  {
    private final Iterator<Map.Entry<String, String>> iterator;

    public Accessor(Iterator<Map.Entry<String, String>> iterator)
    {
      this.iterator = iterator;
    }

    @Override
    public void run()
    {
      while (iterator.hasNext())
      {
        Map.Entry<String, String> entry = iterator.next();
        try
        {
          String st =
              Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
        } catch (Exception e)
        {
          e.printStackTrace();
        }

      }
    }
  }

  private final class Mutator implements Runnable
  {

    private final Random random = new Random();

    private final Iterator<Map.Entry<String, String>> iterator;

    private final Map<String, String> map;

    public Mutator(Map<String, String> map, Iterator<Map.Entry<String, String>> iterator)
    {
      this.map = map;
      this.iterator = iterator;
    }

    @Override
    public void run()
    {
      while (iterator.hasNext())
      {
        try
        {
          iterator.remove();
          this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
        } catch (Exception ex)
        {
          ex.printStackTrace();
        }
      }

    }
  }

  private void run()
  {
    Accessor a1 = new Accessor(this.iterator);
    Accessor a2 = new Accessor(this.iterator);
    Mutator m = new Mutator(map, this.iterator);

    executor.execute(a1);
    executor.execute(m);
    executor.execute(a2);
  }
}
18
Boris Pavlović

つまり、複数のスレッド間でイテレータオブジェクトを共有しないでください。複数のイテレータを作成し、それらを別々のスレッドで同時に使用するのは問題ありません。

11

これ はあなたに良い洞察を与えるかもしれません

ConcurrentHashMapは、呼び出し元への約束をわずかに緩和することにより、より高い並行性を実現します。検索操作は、最後に完了した挿入操作によって挿入された値を返し、同時に進行中の挿入操作によって追加された値を返すこともあります(ただし、無意味な結果を返すことはありません)。 ConcurrentHashMap.iterator()によって返されるイテレータは、各要素を最大で1回返し、ConcurrentModificationExceptionをスローすることはありませんが、イテレータの構築以降に発生した挿入または削除を反映する場合としない場合があります 。コレクションを反復するときにスレッドセーフを提供するために、テーブル全体のロックは必要ありません(または可能ですらありません)。 ConcurrentHashMapは、更新を防ぐためにテーブル全体をロックする機能に依存しないアプリケーションで、synchronizedMapまたはHashtableの代替として使用できます。

これに関して:

ただし、イテレータは、一度に1つのスレッドのみが使用するように設計されています。

つまり、2つのスレッドでConcurrentHashMapによって生成された反復子を使用しても安全ですが、アプリケーションで予期しない結果が生じる可能性があります。

8
nanda

どういう意味ですか?

つまり、2つのスレッドで同じイテレーターを使用しないでください。キー、値、またはエントリを反復処理する必要がある2つのスレッドがある場合、それぞれが独自のイテレータを作成して使用する必要があります。

2つのスレッドで同時にマップを反復しようとするとどうなりますか?

このルールを破るとどうなるかは完全には明らかではありません。 (たとえば)2つのスレッドが同期せずに標準入力から読み取ろうとする場合と同じように、混乱を招く動作をする可能性があります。また、スレッドセーフではない動作が発生する可能性もあります。

しかし、2つのスレッドが異なるイテレーターを使用している場合は、問題ないはずです。

反復中にマップに値を追加または削除するとどうなりますか?

これは別の問題ですが、引用したjavadocセクションが適切に答えています。基本的に、反復子はスレッドセーフですが、定義されていません反復子によって返されるオブジェクトのシーケンスに反映される同時挿入、更新、または削除の影響を確認するかどうか。実際には、マップ内のどこで更新が行われるかによります。

4
Stephen C