¿Es seguro iterar el hilo de valores de ConcurrentHashMap?

156

En javadoc para ConcurrentHashMap está lo siguiente:

Las operaciones de recuperación (incluida la obtención) generalmente no se bloquean, por lo que pueden superponerse con las operaciones de actualización (incluida la colocación y eliminación). Las recuperaciones reflejan los resultados de las operaciones de actualización completadas más recientemente en espera de su inicio. Para operaciones agregadas como putAll y clear, las recuperaciones simultáneas pueden reflejar la inserción o eliminación de solo algunas entradas. Del mismo modo, los iteradores y las enumeraciones devuelven elementos que reflejan el estado de la tabla hash en algún momento en o desde la creación del iterador / enumeración. No lanzan ConcurrentModificationException. Sin embargo, los iteradores están diseñados para ser utilizados por un solo hilo a la vez.

Qué significa eso? ¿Qué sucede si intento iterar el mapa con dos hilos al mismo tiempo? ¿Qué sucede si pongo o elimino un valor del mapa mientras lo itero?

Palo
fuente

Respuestas:

193

Qué significa eso?

Eso significa que cada iterador que obtiene de un ConcurrentHashMapestá diseñado para ser utilizado por un solo subproceso y no debe pasarse. Esto incluye el azúcar sintáctico que proporciona el ciclo for-each.

¿Qué sucede si intento iterar el mapa con dos hilos al mismo tiempo?

Funcionará como se espera si cada uno de los hilos usa su propio iterador.

¿Qué sucede si pongo o elimino un valor del mapa mientras lo itero?

Se garantiza que las cosas no se romperán si haces esto (eso es parte de lo que ConcurrentHashMapsignifica "concurrente" ). Sin embargo, no hay garantía de que un hilo vea los cambios en el mapa que realiza el otro hilo (sin obtener un nuevo iterador del mapa). Se garantiza que el iterador reflejará el estado del mapa en el momento de su creación. Otros cambios pueden reflejarse en el iterador, pero no tienen que serlo.

En conclusión, una declaración como

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

estará bien (o al menos seguro) casi cada vez que lo vea.

Waldheinz
fuente
Entonces, ¿qué sucederá si durante la iteración, otro hilo eliminó un objeto o10 ​​del mapa? ¿Todavía puedo ver o10 en la iteración incluso si se ha eliminado? @Waldheinz
Alex
Como se indicó anteriormente, realmente no se especifica si un iterador existente reflejará cambios posteriores en el mapa. Entonces no lo sé, y por especificación nadie lo hace (sin mirar el código, y eso puede cambiar con cada actualización del tiempo de ejecución). Entonces no puedes confiar en eso.
Waldheinz
8
Pero todavía tengo un ConcurrentModificationExceptiontiempo iterando ConcurrentHashMap, ¿por qué?
Kimi Chiu
@KimiChiu probablemente debería publicar una nueva pregunta que proporcione el código que desencadena esa excepción, pero dudo mucho que se deba directamente a la iteración de un contenedor concurrente. a menos que la implementación de Java tenga errores.
Waldheinz
18

Puede usar esta clase para probar dos hilos de acceso y uno que mute la instancia compartida 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);
  }
}

No se lanzará ninguna excepción.

Compartir el mismo iterador entre hilos de acceso puede conducir a un punto muerto:

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

Tan pronto como comience a compartir lo mismo Iterator<Map.Entry<String, String>>entre los hilos de acceso y mutadores java.lang.IllegalStateException, comenzarán a aparecer.

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ć
fuente
¿Está seguro de que 'Compartir el mismo iterador entre hilos de acceso puede llevar a un punto muerto'? El documento dice que la lectura no está bloqueada y probé su programa y todavía no hay un punto muerto. Aunque el resultado iterativo será incorrecto.
Tony
12

Significa que no debe compartir un objeto iterador entre varios subprocesos. Crear múltiples iteradores y usarlos simultáneamente en hilos separados está bien.

Tuure Laurinolli
fuente
¿Alguna razón por la que no capitalizaste el I en Iterator? Dado que es el nombre de la clase, puede ser menos confuso.
Bill Michell
1
@Bill Michell, ahora estamos en la semántica de publicar la etiqueta. Creo que debería haber hecho de Iterator un enlace de regreso al javadoc para un Iterator, o al menos haberlo colocado dentro de las anotaciones de código en línea (`).
Tim Bender el
10

Esto podría darte una buena idea

ConcurrentHashMap logra una mayor concurrencia relajando ligeramente las promesas que hace a las personas que llaman. Una operación de recuperación devolverá el valor insertado por la operación de inserción completada más reciente, y también puede devolver un valor agregado por una operación de inserción que esté en progreso simultáneamente (pero en ningún caso devolverá un resultado sin sentido). Los iteradores devueltos por ConcurrentHashMap.iterator () devolverán cada elemento una vez como máximo y nunca arrojarán ConcurrentModificationException, pero pueden reflejar o no inserciones o eliminaciones que ocurrieron desde que se construyó el iterador. No es necesario el bloqueo de toda la tabla (o incluso es posible) para proporcionar seguridad de subprocesos al iterar la colección. ConcurrentHashMap se puede usar como reemplazo de syncMap o Hashtable en cualquier aplicación que no dependa de la capacidad de bloquear toda la tabla para evitar actualizaciones.

Con respecto a este:

Sin embargo, los iteradores están diseñados para ser utilizados por un solo hilo a la vez.

Esto significa que, si bien el uso de iteradores producidos por ConcurrentHashMap en dos subprocesos es seguro, puede causar un resultado inesperado en la aplicación.

nanda
fuente
4

Qué significa eso?

Significa que no debe intentar usar el mismo iterador en dos hilos. Si tiene dos hilos que necesitan iterar sobre las claves, valores o entradas, cada uno debe crear y usar sus propios iteradores.

¿Qué sucede si intento iterar el mapa con dos hilos al mismo tiempo?

No está del todo claro qué sucedería si infringe esta regla. Podría obtener un comportamiento confuso, de la misma manera que lo hace si (por ejemplo) dos hilos intentan leer desde la entrada estándar sin sincronizar. También podría obtener un comportamiento no seguro para subprocesos.

Pero si los dos hilos utilizan iteradores diferentes, debería estar bien.

¿Qué sucede si pongo o elimino un valor del mapa mientras lo itero?

Ese es un tema separado, pero la sección de javadoc que citó lo responde adecuadamente. Básicamente, los iteradores son seguros para subprocesos, pero no está definido si verá los efectos de cualquier inserción, actualización o eliminación simultánea reflejada en la secuencia de objetos devueltos por el iterador. En la práctica, probablemente depende de en qué lugar del mapa ocurran las actualizaciones.

Stephen C
fuente