L'itération du thread des valeurs ConcurrentHashMap est-elle sûre?


Dans javadoc pour ConcurrentHashMap est le suivant:

Les opérations de récupération (y compris get) ne bloquent généralement pas, elles peuvent donc se chevaucher avec les opérations de mise à jour (y compris put et remove). Les récupérations reflètent les résultats des opérations de mise à jour les plus récentes qui se tiennent au début. Pour les opérations d'agrégation telles que putAll et clear, les récupérations simultanées peuvent refléter l'insertion ou la suppression de seulement certaines entrées. De même, les itérateurs et les énumérations renvoient éléments reflétant l'état de la table de hachage à un moment donné ou depuis la création de l'itérateur/énumération. Ils ne lancent pas ConcurrentModificationException. Cependant, les itérateurs sont conçus pour être utilisés par un seul thread à la fois.

Qu'est-ce que cela signifie? Qu'advient-il si j'essaie d'itérer la carte avec deux threads en même temps? Que se passe-t-il si je mets ou supprime une valeur de la carte tout en l'itérant?

Author: Steve Armstrong, 2010-09-22

5 answers

Qu'est-ce que cela signifie?

Cela signifie que chaque itérateur que vous obtenez à partir d'un ConcurrentHashMap est conçu pour être utilisé par un seul thread et ne doit pas être transmis. Cela inclut le sucre syntaxique fourni par la boucle for-each.

Ce qui se passe si j'essaie d'itérer la carte avec deux threads en même temps?

Cela fonctionnera comme prévu si chacun des threads utilise son propre itérateur.

Que se passe - t-il si je mets ou supprime une valeur de la carte lors de l'itération il?

Il est garanti que les choses ne se casseront pas si vous faites cela (cela fait partie de ce que signifie le "concurrent" dans ConcurrentHashMap). Cependant, il n'y a aucune garantie qu'un thread verra les modifications apportées à la carte que l'autre thread effectue (sans obtenir un nouvel itérateur à partir de la carte). L'itérateur est garanti pour refléter l'état de la carte au moment de sa création. D'autres changements peuvent être reflétés dans l'itérateur, mais ils ne doivent pas l'être.

Dans conclusion, une déclaration comme

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

Sera bien (ou au moins sûr) presque chaque fois que vous le voyez.

 162
Author: Waldheinz, 2012-02-04 02:35:53

Vous pouvez utiliser cette classe pour tester deux threads accédants et un mutant l'instance partagée de 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);
  }
}

Aucune exception ne sera levée.

Le partage du même itérateur entre les threads d'accès peut entraîner un blocage:

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

Dès que vous commencez à partager le même Iterator<Map.Entry<String, String>>entre les threads accessor et mutator, les java.lang.IllegalStateException commencent à apparaître.

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);
  }
}
 17
Author: Boris Pavlović, 2010-09-22 11:26:02

Cela signifie que vous ne devez pas partager un objet itérateur entre plusieurs threads. Créer plusieurs itérateurs et les utiliser simultanément dans des threads séparés est très bien.

 9
Author: Tuure Laurinolli, 2010-09-22 11:14:22

Ce pourrait vous donner un bon aperçu

ConcurrentHashMap atteint une concurrence plus élevée en assouplissant légèrement les promesses qu'il fait aux appelants. Une opération de récupération renverra la valeur insérée par l'opération d'insertion terminée la plus récente, et peut également renvoyer une valeur ajoutée par une opération d'insertion en cours (mais en aucun cas elle ne renverra un résultat non-sens). Itérateurs retournés par ConcurrentHashMap.iterator() retournera chaque élément une fois au plus et ne lancera jamais ConcurrentModificationException, mais peut ou non refléter les insertions ou les suppressions survenues depuis la construction de l'itérateur. Aucun verrouillage à l'échelle de la table n'est nécessaire (ou même possible) pour assurer la sécurité des threads lors de l'itération de la collection. ConcurrentHashMap peut être utilisé en remplacement de synchronizedMap ou Hashtable dans toute application qui ne repose pas sur la possibilité de verrouiller la table entière pour empêcher les mises à jour.

Concernant ceci:

Cependant, les itérateurs sont conçus pour être utilisés par un seul thread à la fois.

Cela signifie que, bien que l'utilisation d'itérateurs produits par ConcurrentHashMap dans deux threads soit sûre, cela peut provoquer un résultat inattendu dans l'application.

 6
Author: nanda, 2010-09-22 11:17:48

Qu'est-ce que cela signifie?

Cela signifie que vous ne devriez pas essayer d'utiliser la même itérateur dans deux threads. Si vous avez deux threads qui doivent itérer sur les clés, les valeurs ou les entrées, ils doivent chacun créer et utiliser leurs propres itérateurs.

Ce qui se passe si j'essaie d'itérer la carte avec deux threads en même temps?

Il n'est pas tout à fait clair ce qui se passerait si vous enfreigniez cette règle. Vous pourriez simplement avoir un comportement déroutant, de la même manière que vous faites si (par exemple) deux threads essaient de lire à partir d'une entrée standard sans synchronisation. Vous pouvez également obtenir un comportement non-thread-safe.

Mais si les deux threads utilisaient des itérateurs différents, cela devrait aller.

Que se passe - t-il si je mets ou supprime une valeur de la carte tout en l'itérant?

C'est un problème distinct, mais la section javadoc que vous avez citée y répond correctement. Fondamentalement, les itérateurs sont thread-safe, mais , il n'est pas défini indique si vous verrez les effets des insertions, mises à jour ou suppressions simultanées reflétés dans la séquence d'objets renvoyés par l'itérateur. En pratique, cela dépend probablement de l'endroit où les mises à jour se produisent sur la carte.

 4
Author: Stephen C, 2010-09-22 12:05:21