web-dev-qa-db-fra.com

Les valeurs ConcurrentHashMap itératives sont-elles thread-safe?

Dans javadoc for ConcurrentHashMap , voici ce qui suit:

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 les opérations de vente et de suppression). Les récupérations reflètent les résultats des dernières opérations de mise à jour terminées depuis le 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 certaines entrées uniquement. 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 lancent 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'essaye 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 en l'itérant?

136
Palo

Qu'est-ce que ça veut dire?

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.

Que se passe-t-il si j'essaye 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 en l'itérant?

Il est garanti que les choses ne se briseront pas si vous faites cela (cela fait partie de ce que "simultané" dans ConcurrentHashMap signifie). Cependant, rien ne garantit qu'un thread verra les modifications apportées à la carte par l'autre (sans obtenir un nouvel itérateur à partir de la carte). Il est garanti que l’itérateur reflète 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 nécessairement l'être.

En conclusion, une déclaration comme

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

ira bien (ou au moins sans danger) presque chaque fois que vous le voyez.

172
Waldheinz

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.

Partager le 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>> parmi les threads d’accesseur et de mutateur Java.lang.IllegalStateExceptions va 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);
  }
}
18
Boris Pavlović

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 acceptable.

11
Tuure Laurinolli

This pourrait vous donner un bon aperçu

ConcurrentHashMap réalise une concurrence accrue 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 simultanément en cours (mais elle ne renverra en aucun cas un résultat absurde). Les itérateurs renvoyés par ConcurrentHashMap.iterator () renverront chaque élément au plus une fois et ne liront jamais ConcurrentModificationException, mais peuvent ou non refléter des insertions ou des suppressions intervenues 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 sujet:

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 sans danger, cela peut entraîner un résultat inattendu dans l'application.

8
nanda

Qu'est-ce que ça veut dire?

Cela signifie que vous ne devriez 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'essaye d'itérer la carte avec deux threads en même temps?

Ce qui se passerait si vous enfreignez cette règle n'est pas tout à fait clair. Vous pouvez simplement avoir un comportement déroutant, de la même manière que si (par exemple) deux threads essayaient de lire à partir d'une entrée standard sans se synchroniser. 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 en l'itérant?

C'est une question distincte, mais la section relative au javadoc que vous avez citée y répond de manière adéquate. Fondamentalement, les itérateurs sont thread-safe, mais non défini si vous verrez les effets d’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 ont lieu dans la carte.

4
Stephen C