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


156

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

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

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

回答:


193

どういう意味ですか?

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

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

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

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

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

結論として、次のようなステートメント

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

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


では、反復中に別のスレッドがオブジェクトo10をマップから削除するとどうなりますか?o10が削除されていても、イテレーションで引き続き表示できますか?@Waldheinz
Alex

上記のように、既存のイテレータがマップへのその後の変更を反映するかどうかは実際には指定されていません。だから私は知りません、そして仕様上誰もしません(コードを見ない限り、ランタイムの更新ごとに変わる可能性があります)。だからあなたはそれに頼ることはできません。
Waldheinz、2015年

8
しかし、私はまだConcurrentModificationExceptionしばらく繰り返しますConcurrentHashMap、なぜですか?
Kimi Chiu 2017年

@KimiChiuあなたはおそらく、その例外をトリガーするコードを提供する新しい質問を投稿する必要がありますが、それは並行コンテナーの反復から直接発生することは非常に疑わしいです。Java実装にバグがない限り。
Waldheinz

18

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

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.IllegalStateExceptionポップアップが表示され始めます。

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);
  }
}

「アクセサスレッド間で同じイテレータを共有するとデッドロックが発生する可能性があります」について確信がありますか?文書には読み取りがブロックされていないと書いてあり、私はあなたのプログラムを試しましたが、まだデッドロックは発生していません。繰り返しの結果は間違っていますが。
トニー

12

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


イテレーターでIを大文字にしていない理由は何ですか?これはクラスの名前なので、混乱しにくいかもしれません。
ビルミシェル

1
@Bill Michell、エチケットを投稿するセマンティクスになりました。彼はIteratorをIteratorのjavadocへのリンクにするか、少なくともインラインコードアノテーション( `)内に配置する必要があったと思います。
Tim Bender

10

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

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

これに関して:

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

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


4

どういう意味ですか?

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

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

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

ただし、2つのスレッドが異なるイテレータを使用している場合は、問題ありません。

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

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

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