webentwicklung-frage-antwort-db.com.de

Ist das Durchlaufen von ConcurrentHashMap-Werten threadsicher?

In Javadoc für ConcurrentHashMap ist das folgende:

Abrufvorgänge (einschließlich get) werden im Allgemeinen nicht blockiert, sodass sie sich mit Aktualisierungsvorgängen (einschließlich put und remove) überschneiden können. Abfragen spiegeln die Ergebnisse der zuletzt abgeschlossenen Aktualisierungsvorgänge wider, die bei ihrem Beginn angehalten wurden. Bei zusammengefassten Vorgängen wie putAll und clear wird beim gleichzeitigen Abrufen möglicherweise nur ein Teil der Einträge eingefügt oder entfernt. In ähnlicher Weise geben Iteratoren und Aufzählungen Elemente zurück, die den Status der Hash-Tabelle zu einem bestimmten Zeitpunkt oder seit der Erstellung des Iterators/der Aufzählung widerspiegeln. Sie lösen keine ConcurrentModificationException aus. Iteratoren können jedoch jeweils nur von einem Thread verwendet werden.

Was heißt das? Was passiert, wenn ich versuche, die Map mit zwei Threads gleichzeitig zu durchlaufen? Was passiert, wenn ich während der Iteration einen Wert in die Karte einfüge oder von ihr entferne?

136
Palo

Was heißt das?

Das bedeutet, dass jeder Iterator, den Sie von einem ConcurrentHashMap erhalten, für die Verwendung durch einen einzelnen Thread ausgelegt ist und nicht weitergegeben werden sollte. Dies schließt den syntaktischen Zucker ein, den die for-each-Schleife bereitstellt.

Was passiert, wenn ich versuche, die Map mit zwei Threads gleichzeitig zu durchlaufen?

Es funktioniert wie erwartet, wenn jeder der Threads seinen eigenen Iterator verwendet.

Was passiert, wenn ich während der Iteration einen Wert in die Karte einfüge oder von ihr entferne?

Es ist garantiert, dass die Dinge nicht kaputt gehen, wenn Sie dies tun (das ist ein Teil dessen, was das "gleichzeitige" in ConcurrentHashMap bedeutet). Es gibt jedoch keine Garantie dafür, dass ein Thread die Änderungen an der Karte sieht, die der andere Thread ausführt (ohne einen neuen Iterator von der Karte zu erhalten). Es ist garantiert, dass der Iterator den Status der Karte zum Zeitpunkt ihrer Erstellung widerspiegelt. Weitere Änderungen können sich im Iterator widerspiegeln, müssen es aber nicht.

Abschließend eine Aussage wie

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

wird in Ordnung (oder zumindest sicher) sein, fast jedes Mal, wenn Sie es sehen.

172
Waldheinz

Sie können diese Klasse verwenden, um zwei zugreifende Threads und einen zu testen, der die gemeinsam genutzte Instanz von ConcurrentHashMap mutiert:

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

Es wird keine Ausnahme geworfen.

Das Teilen des gleichen Iterators zwischen Accessor-Threads kann zu einem Deadlock führen:

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

Sobald Sie anfangen, das gleiche Iterator<Map.Entry<String, String>> zwischen Accessor- und Mutator-Threads Java.lang.IllegalStateExceptions wird auftauchen.

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ć

Dies bedeutet, dass Sie ein Iteratorobjekt nicht für mehrere Threads freigeben sollten. Das Erstellen mehrerer Iteratoren und deren gleichzeitige Verwendung in separaten Threads ist in Ordnung.

11

Dies könnte Ihnen einen guten Einblick geben

ConcurrentHashMap erzielt eine höhere Parallelität, indem es die Versprechungen, die es an Anrufer macht, etwas lockert. Eine Abrufoperation gibt den Wert zurück, der durch die zuletzt abgeschlossene Einfügeoperation eingefügt wurde, und möglicherweise auch einen Wert, der durch eine gleichzeitig ausgeführte Einfügeoperation hinzugefügt wurde (in keinem Fall wird jedoch ein Unsinnergebnis zurückgegeben). Iteratoren, die von ConcurrentHashMap.iterator () zurückgegeben werden, geben jedes Element höchstens einmal zurück und lösen niemals eine ConcurrentModificationException aus, können aber Einfügungen oder Entfernungen widerspiegeln, die seit der Erstellung des Iterators aufgetreten sind . Es ist keine tabellenweite Sperre erforderlich (oder sogar möglich), um Threadsicherheit beim Durchlaufen der Auflistung zu gewährleisten. ConcurrentHashMap kann als Ersatz für synchronizedMap oder Hashtable in jeder Anwendung verwendet werden, die nicht auf die Möglichkeit angewiesen ist, die gesamte Tabelle zu sperren, um Aktualisierungen zu verhindern.

Was das betrifft:

Iteratoren können jedoch jeweils nur von einem Thread verwendet werden.

Das bedeutet, dass die Verwendung von Iteratoren, die von ConcurrentHashMap in zwei Threads erstellt wurden, sicher ist und zu unerwarteten Ergebnissen in der Anwendung führen kann.

8
nanda

Was heißt das?

Es bedeutet, dass Sie nicht versuchen sollten, denselben Iterator in zwei Threads zu verwenden. Wenn Sie zwei Threads haben, die über die Schlüssel, Werte oder Einträge iterieren müssen, sollten sie jeweils ihre eigenen Iteratoren erstellen und verwenden.

Was passiert, wenn ich versuche, die Map mit zwei Threads gleichzeitig zu durchlaufen?

Es ist nicht ganz klar, was passieren würde, wenn Sie gegen diese Regel verstoßen würden. Sie könnten genauso verwirrendes Verhalten bekommen, wie Sie es tun, wenn (zum Beispiel) zwei Threads versuchen, von der Standardeingabe zu lesen, ohne zu synchronisieren. Es kann auch zu nicht threadsicherem Verhalten kommen.

Aber wenn die beiden Threads unterschiedliche Iteratoren verwenden, sollten Sie in Ordnung sein.

Was passiert, wenn ich während der Iteration einen Wert in die Karte einbaue oder daraus entferne?

Das ist ein separates Problem, aber der Javadoc-Abschnitt, den Sie zitiert haben, beantwortet es angemessen. Grundsätzlich sind die Iteratoren threadsicher, aber es ist nicht definiert ob Sie die Auswirkungen von gleichzeitigen Einfügungen, Aktualisierungen oder Löschungen sehen, die sich in der Reihenfolge der vom Iterator zurückgegebenen Objekte widerspiegeln. In der Praxis hängt es wahrscheinlich davon ab, wo in der Karte die Aktualisierungen vorgenommen werden.

4
Stephen C