java - safe - concurrenthashmap example
¿La iteración de los valores de ConcurrentHashMap es segura? (5)
En javadoc para ConcurrentHashMap es el siguiente:
Las operaciones de recuperación (incluido get) generalmente no se bloquean, por lo que pueden superponerse con las operaciones de actualización (incluso poner y eliminar). 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 eso? ¿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?
Qué significa eso?
Significa que no deberías tratar de usar el mismo iterador en dos hilos. 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 incumpliera 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 diferentes iteradores, 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ó adecuadamente lo responde. Básicamente, los iteradores son seguros para la ejecución de subprocesos, pero no se define 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.
Qué significa eso?
Esto significa que cada iterador que obtenga de un ConcurrentHashMap
está diseñado para ser utilizado por un único hilo y no debe pasarse por alto. 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 espera 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 haces esto (eso es parte de lo que significa el "concurrente" en ConcurrentHashMap
). 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()) {
// ...
}
estará bien (o al menos seguro) casi cada vez que lo veas.
Puede utilizar esta clase para probar dos subprocesos 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);
}
}
No se lanzará ninguna excepción.
Compartir el mismo iterador entre los subprocesos de acceso puede llevar a un interbloqueo:
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 el mismo Iterator<Map.Entry<String, String>>
entre accesor y subprocesos de mutador 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);
}
}
Significa que no debe compartir un objeto iterador entre múltiples hilos. Crear múltiples iteradores y usarlos simultáneamente en hilos separados está bien.
This podría darte una buena idea
ConcurrentHashMap logra una mayor concurrencia al relajar 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 este:
Sin embargo, los iteradores están diseñados para ser utilizados solo por un hilo a la vez.
Esto significa que, al usar iteradores producidos por ConcurrentHashMap en dos hilos son seguros, puede causar un resultado inesperado en la aplicación.