113

En Javadoc para ConcurrentHashMap es la siguiente:¿La iteración de los valores de ConcurrentHashMap es segura?

operaciones de recuperación (incluyendo GET) por lo general no bloquear, por lo que puede superponerse con las operaciones de actualización (incluyendo poner y quitar). Las recuperaciones reflejan los resultados de las operaciones de actualización completadas más recientemente que se mantienen desde 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. De forma similar, Iteradores y Enumeraciones devuelven elementos que reflejan el estado de la tabla hash en algún punto en o desde la creación del iterador/enumeración. No lanzan ConcurrentModificationException. Sin embargo, los iteradores están diseñados para ser utilizados solo por un hilo a la vez.

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

Respuesta

147

¿Qué significa?

Eso significa que cada iterador que obtenga de un ConcurrentHashMap está diseñado para ser utilizado por un solo hilo y no debe ser pasada alrededor. Esto incluye el azúcar sintáctico que proporciona el bucle for-each.

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

Funcionará como se esperaba si cada uno de los subprocesos 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 lo hace (que es parte de lo que el "concurrente" en ConcurrentHashMap medios). 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 refleja 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()) { 
    // ... 
} 

va a estar bien (o al menos seguro) casi cada vez que la vea.

+0

Entonces, ¿qué pasará 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

+0

Como se indicó anteriormente, realmente no se especifica si un iterador existente reflejará cambios posteriores en el mapa. Así que no 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

+4

Pero todavía tengo una 'ConcurrentModificationException' mientras itero un' ConcurrentHashMap', ¿por qué? –

5

This podría darle una buena visión

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 completa más reciente, y también puede devolver un valor agregado mediante 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 lanzarán ConcurrentModificationException, pero pueden o no reflejar inserciones o eliminaciones que ocurrieron desde que se construyó el iterador. No se necesita ningún bloqueo en toda la tabla (o incluso posible) para proporcionar seguridad de subprocesos al iterar la colección. ConcurrentHashMap se puede usar como reemplazo de synchronizedMap o Hashtable en cualquier aplicación que no dependa de la capacidad de bloquear toda la tabla para evitar actualizaciones.

Con respecto a esto:

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

Significa que, si bien los iteradores producidos por ConcurrentHashMap en dos hilos son seguros, puede causar un resultado inesperado en la aplicación.

8

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

+0

¿Alguna razón por la que no capitalizó el I en Iterador?Dado que es el nombre de la clase, podría ser menos confuso. –

+1

@Bill Michell, ahora estamos en la semántica de publicar etiqueta. Creo que debería haber hecho de Iterator un enlace al javadoc para un iterador, o al menos colocarlo dentro de las anotaciones de código en línea ('). –

17

Es posible utilizar esta clase para probar dos hilos de acceso y una mutación de 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); 
    } 
} 

Sin excepción será lanzada.

Compartiendo el mismo repetidor entre los hilos de descriptor de acceso puede llevar 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 empiece a compartir la misma Iterator<Map.Entry<String, String>> entre los hilos accesores o modificadores java.lang.IllegalStateException s 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); 
    } 
} 
+0

¿Estás seguro de que 'Compartir el mismo iterador entre los subprocesos de acceso puede llevar a un interbloqueo'? El documento dice que la lectura no está bloqueada y probé tu programa y aún no ha ocurrido ningún punto muerto. Aunque el resultado de iteración será incorrecto. – Tony

4

¿Qué significa?

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

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

No está del todo claro qué pasaría si incumplía esta regla. Puede obtener un comportamiento confuso, del mismo modo que si (por ejemplo) dos hilos intentan leer desde la entrada estándar sin sincronizar. También podría obtener un comportamiento que no sea 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 problema aparte, 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 las inserciones, actualizaciones o eliminaciones simultáneas reflejadas en la secuencia de objetos devueltos por el iterador. En la práctica, probablemente dependa de en qué parte del mapa se encuentren las actualizaciones.

Cuestiones relacionadas