173 votes

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

Dans la javadoc de ConcurrentHashMap est le suivant :

Les opérations de récupération (y compris get) ne sont généralement pas bloquées et peuvent donc chevaucher 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écemment achevées, en les maintenant dès leur 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 certaines entrées seulement. 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é, lors de la création de l'itérateur/de l'énumération ou depuis celle-ci. Ils ne lèvent pas d'exception ConcurrentModificationException. Cependant, les itérateurs sont conçus pour être utilisés par un seul thread à la fois.

Qu'est-ce que cela signifie ? 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 retire une valeur de la carte pendant que je l'itère ?

218voto

Waldheinz Points 6308

Qu'est-ce que cela signifie ?

Cela signifie que chaque itérateur que vous obtenez d'un ConcurrentHashMap est conçu pour être utilisé par un seul thread et ne doit pas être transmis. Cela inclut le sucre syntaxique que fournit 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 enlève une valeur de la carte tout en l'itérant ?

Il est garanti que les choses ne se casseront pas si vous faites cela (c'est une partie de ce que le "concurrent" dans ConcurrentHashMap moyens). Cependant, il n'y a aucune garantie qu'un thread verra les modifications apportées à la carte par l'autre thread (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. Les modifications ultérieures peuvent être reflétées dans l'itérateur, mais ce n'est pas une obligation.

En conclusion, une déclaration comme

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

sera bon (ou au moins sûr) presque à chaque fois que vous le verrez.

0 votes

Que se passera-t-il donc si, pendant l'itération, un autre thread a retiré un objet o10 de la carte ? Est-ce que je peux toujours voir o10 dans l'itération même s'il a été retiré ? @Waldheinz

0 votes

Comme indiqué ci-dessus, il n'est vraiment pas précisé si un itérateur existant reflétera les modifications ultérieures de la carte. Je ne le sais donc pas, et par spécification personne ne le sait (sans regarder le code, et cela peut changer à chaque mise à jour du runtime). Vous ne pouvez donc pas vous y fier.

12 votes

Mais j'ai toujours un ConcurrentModificationException tout en itérant un ConcurrentHashMap Pourquoi ?

18voto

Boris Pavlović Points 22207

Vous pouvez utiliser cette classe pour tester deux threads accédant 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 conduire à 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 Iterator<Map.Entry<String, String>> entre les threads accesseurs et mutateurs java.lang.IllegalStateException vont commencer à 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);
  }
}

0 votes

Êtes-vous sûr de la phrase "Partager le 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 s'est encore produit. Bien que le résultat de l'itération soit faux.

12voto

Tuure Laurinolli Points 1391

Cela signifie que vous ne devez pas partager un objet itérateur entre plusieurs threads. Il est possible de créer plusieurs itérateurs et de les utiliser simultanément dans des threads distincts.

0 votes

Pourquoi n'avez-vous pas mis la majuscule au I de Iterator ? Comme c'est le nom de la classe, cela pourrait prêter à confusion.

1 votes

@Bill Michell, maintenant nous sommes dans la sémantique de l'étiquette du postage. Je pense qu'il aurait dû faire d'Iterator un lien vers la javadoc pour un Iterator, ou au moins le placer à l'intérieur des annotations de code en ligne (`).

10voto

nanda Points 12764

Ce site pourrait vous donner un bon aperçu

ConcurrentHashMap atteint un niveau de concurrence plus élevé 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 la plus récente opération d'insertion terminée, et peut également renvoyer une valeur ajoutée par une opération d'insertion qui est en cours simultanément (mais en aucun cas elle ne renverra un résultat absurde). Les itérateurs renvoyés par ConcurrentHashMap.iterator() renverront chaque élément une fois au maximum et ne lèveront jamais d'exception ConcurrentModificationException, mais ils peuvent 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 (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 dépend pas de la possibilité de verrouiller la table entière pour empêcher les mises à jour.

A propos de ça :

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

Cela signifie que, même si l'utilisation des itérateurs produits par ConcurrentHashMap dans deux threads est sûre, elle peut entraîner un résultat inattendu dans l'application.

5voto

Stephen C Points 255558

Qu'est-ce que cela signifie ?

Cela signifie que vous ne devez pas essayer d'utiliser le même itérateur dans deux threads. Si deux threads ont besoin d'itérer sur 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 ?

On ne sait pas exactement ce qui se passerait si vous enfreigniez cette règle. Vous pourriez simplement obtenir un comportement confus, de la même manière que si (par exemple) deux threads essayent de lire l'entrée standard sans se synchroniser. Vous pourriez également obtenir un comportement non sécurisé pour les threads.

Mais si les deux threads ont utilisé des itérateurs différents, tout devrait bien se passer.

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

Si les deux threads utilisent le même itérateur : voir ci-dessus. Vous risquez d'obtenir un comportement confus et peut-être non sécurisé pour les threads.

Si les threads utilisent des itérateurs différents, la section de la javadoc que vous avez citée y répond de manière adéquate. Fondamentalement, il n'est pas défini si un thread / itérateur verra les effets des insertions, mises à jour ou suppressions concurrentes effectuées par l'autre thread / itérateur. Cependant, les insertions / mises à jour / suppressions seront effectuées en fonction des propriétés de concurrence de la carte.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X