L'itération des valeurs ConcurrentHashMap est-elle sûre pour les threads?

156

Dans javadoc pour ConcurrentHashMap est le suivant:

Les opérations de récupération (y compris get) ne bloquent généralement pas, et peuvent donc se chevaucher avec les opérations de mise à jour (y compris put et remove). Les extractions reflètent les résultats des opérations de mise à jour les plus récemment terminées se tenant à leur début. Pour les opérations d'agrégation telles que putAll et clear, les extractions 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 des é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 lèvent pas ConcurrentModificationException. Cependant, les itérateurs sont conçus pour être utilisés par un seul thread à la fois.

Qu'est-ce que ça veut dire? Que se passe-t-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 lors de son itération?

Palo
la source

Réponses:

193

Qu'est-ce que ça veut dire?

Cela signifie que chaque itérateur obtenu à partir de a ConcurrentHashMapest 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.

Que se passe-t-il 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 son itération?

Il est garanti que les choses ne se casseront pas si vous faites cela (cela fait partie de ce que signifie le «concurrent» 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 de la carte). L'itérateur est garanti pour refléter l'état de la carte au moment de sa création. Des modifications ultérieures peuvent être reflétées dans l'itérateur, mais elles ne doivent pas l'être.

En conclusion, une déclaration comme

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

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

Waldheinz
la source
Alors que se passera-t-il si pendant l'itération, un autre thread supprimait un objet o10 de la carte? Puis-je toujours voir o10 dans l'itération même s'il a été supprimé? @Waldheinz
Alex
Comme indiqué ci-dessus, il n'est vraiment pas spécifié si un itérateur existant reflétera les modifications ultérieures de la carte. Donc je ne sais pas, et par spécification personne ne le fait (sans regarder le code, et cela peut changer à chaque mise à jour du runtime). Vous ne pouvez donc pas vous y fier.
Waldheinz
8
Mais j'ai encore du ConcurrentModificationExceptiontemps à répéter un ConcurrentHashMap, pourquoi?
Kimi Chiu
@KimiChiu vous devriez probablement poster une nouvelle question fournissant le code déclenchant cette exception, mais je doute fortement qu'elle découle directement de l'itération d'un conteneur concurrent. sauf si l'implémentation Java est boguée.
Waldheinz
18

Vous pouvez utiliser cette classe pour tester deux threads d'accès 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 lancée.

Le partage du même itérateur entre les threads d'accesseurs 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 la même chose Iterator<Map.Entry<String, String>>entre les threads accesseurs et mutateurs, les threads java.lang.IllegalStateExceptioncommenceront à 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);
  }
}
Boris Pavlović
la source
Êtes-vous sûr que «le partage du même itérateur entre les threads d'accès peut conduire à un blocage»? Le document dit que la lecture n'est pas bloquée et j'ai essayé votre programme et aucun blocage ne se produit encore. Bien que le résultat de l'itération soit faux.
Tony
12

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.

Tuure Laurinolli
la source
Une raison pour laquelle vous n'avez pas capitalisé le I dans Iterator? Puisqu'il s'agit du nom de la classe, cela peut être moins déroutant.
Bill Michell
1
@Bill Michell, nous sommes maintenant dans la sémantique de l'étiquette de publication. Je pense qu'il aurait dû faire d'Iterator un lien vers le javadoc pour un Iterator, ou à tout le moins le placer dans les annotations de code en ligne (`).
Tim Bender
10

Cela pourrait vous donner un bon aperçu

ConcurrentHashMap atteint une plus grande concurrence en relâchant légèrement les promesses qu'il fait aux appelants. Une opération d'extraction 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 qui est simultanément en cours (mais en aucun cas elle ne renverra un résultat absurde). Les itérateurs retournés par ConcurrentHashMap.iterator () renverront chaque élément une fois au plus et ne lèveront jamais ConcurrentModificationException, mais peuvent ou non refléter des insertions ou des suppressions qui se sont produites depuis la construction de l'itérateur. Aucun verrouillage à l'échelle de la table n'est nécessaire (ni 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.

À ce propos:

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

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

nanda
la source
4

Qu'est-ce que ça veut dire?

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

Que se passe-t-il si j'essaie d'itérer la carte avec deux threads en même temps?

Ce qui se passerait si vous enfreigniez cette règle n’est pas tout à fait clair. Vous pourriez juste avoir un comportement déroutant, de la même manière que vous le 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, ça devrait aller.

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

C'est un problème distinct, mais la section javadoc que vous avez citée y répond adéquatement. Fondamentalement, les itérateurs sont thread-safe, mais il n'est pas défini si vous verrez les effets des insertions, mises à jour ou suppressions simultanées reflétées 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.

Stephen C
la source