java performance concurrency concurrenthashmap

java - ¿Debería verificar si el mapa contiene clave antes de usar putIfAbsent de ConcurrentMap?



performance concurrency (6)

He estado utilizando el ConcurrentMap de Java para un mapa que se puede usar desde múltiples hilos. PutIfAbsent es un método excelente y es mucho más fácil de leer / escribir que utilizar operaciones de mapa estándar. Tengo un código que se ve así:

ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>(); // ... map.putIfAbsent(name, new HashSet<X>()); map.get(name).add(Y);

En cuanto a la legibilidad, esto es genial, pero requiere crear un nuevo HashSet siempre, incluso si ya está en el mapa. Podría escribir esto:

if (!map.containsKey(name)) { map.putIfAbsent(name, new HashSet<X>()); } map.get(name).add(Y);

Con este cambio, pierde un poco de legibilidad, pero no necesita crear el HashSet todo el tiempo. ¿Cuál es mejor en este caso? Tiendo a estar del lado del primero ya que es más legible. El segundo funcionaría mejor y podría ser más correcto. Tal vez hay una mejor manera de hacer esto que cualquiera de estos.

¿Cuál es la mejor práctica para usar un putIfAbsent de esta manera?


Al mantener un valor preinicializado para cada hilo, puede mejorar la respuesta aceptada:

Set<X> initial = new HashSet<X>(); ... Set<X> set = map.putIfAbsent(name, initial); if (set == null) { set = initial; initial = new HashSet<X>(); } set.add(Y);

Recientemente utilicé esto con los valores del mapa AtomicInteger en lugar de Set.


En más de 5 años, no puedo creer que nadie haya mencionado o publicado una solución que use ThreadLocal para resolver este problema; y varias de las soluciones en esta página no son seguras y son simplemente descuidadas.

El uso de ThreadLocals para este problema específico no solo se considera las mejores prácticas para la concurrencia, sino también para minimizar la creación de basura / objetos durante la contención del hilo. Además, es un código increíblemente limpio.

Por ejemplo:

private final ThreadLocal<HashSet<X>> threadCache = new ThreadLocal<HashSet<X>>() { @Override protected HashSet<X> initialValue() { return new HashSet<X>(); } }; private final ConcurrentMap<String, Set<X>> map = new ConcurrentHashMap<String, Set<X>>();

Y la lógica real ...

// minimize object creation during thread contention final Set<X> cached = threadCache.get(); Set<X> data = map.putIfAbsent("foo", cached); if (data == null) { // reset the cached value in the ThreadLocal listCache.set(new HashSet<X>()); data = cached; } // make sure that the access to the set is thread safe synchronized(data) { data.add(object); }


La concurrencia es difícil. Si se va a molestar con mapas concurrentes en lugar de un bloqueo directo, también puede intentarlo. De hecho, no hagas búsquedas más de lo necesario.

Set<X> set = map.get(name); if (set == null) { final Set<X> value = new HashSet<X>(); set = map.putIfAbsent(name, value); if (set == null) { set = value; } }

(Descargo de responsabilidad habitual de : fuera de mi cabeza. No probado. No compilado, etc.)

Actualización: 1.8 ha agregado el método predeterminado computeIfAbsent a ConcurrentMap (y Map que es algo interesante porque esa implementación sería incorrecta para ConcurrentMap ). (Y 1.7 agregó el "operador de diamantes" <> ).

Set<X> set = map.computeIfAbsent(name, n -> new HashSet<>());

(Tenga en cuenta que usted es responsable de la seguridad de las hebras de cualquier operación de HashSet contenida en el ConcurrentMap ).


La respuesta de Tom es correcta en lo que respecta al uso de API para ConcurrentMap. Una alternativa que evita el uso de putIfAbsent es usar el mapa informático de GoogleCollections / Guava MapMaker que rellena automáticamente los valores con una función suministrada y maneja toda la seguridad de los hilos para usted. En realidad, solo crea un valor por clave y si la función de creación es costosa, otros hilos que pidan obtener la misma clave se bloquearán hasta que el valor esté disponible.

Editar desde Guava 11, MapMaker está en desuso y está siendo reemplazado por las cosas de Caché / LocalCache / CacheBuilder. Esto es un poco más complicado en su uso, pero básicamente isomorfo.


Mi aproximación genérica:

public class ConcurrentHashMapWithInit<K, V> extends ConcurrentHashMap<K, V> { private static final long serialVersionUID = 42L; public V initIfAbsent(final K key) { V value = get(key); if (value == null) { value = initialValue(); final V x = putIfAbsent(key, value); value = (x != null) ? x : value; } return value; } protected V initialValue() { return null; } }

Y como ejemplo de uso:

public static void main(final String[] args) throws Throwable { ConcurrentHashMapWithInit<String, HashSet<String>> map = new ConcurrentHashMapWithInit<String, HashSet<String>>() { private static final long serialVersionUID = 42L; @Override protected HashSet<String> initialValue() { return new HashSet<String>(); } }; map.initIfAbsent("s1").add("chao"); map.initIfAbsent("s2").add("bye"); System.out.println(map.toString()); }


Puede usar MutableMap.getIfAbsentPut(K, Function0<? extends V>) de Eclipse Collections (anteriormente GS Collections ).

La ventaja de llamar a get() , hacer una comprobación nula y llamar a putIfAbsent() es que solo calcularemos el hashCode de la clave una vez y encontraremos el lugar correcto en la tabla hash una vez. En ConcurrentMaps como org.eclipse.collections.impl.map.mutable.ConcurrentHashMap , la implementación de getIfAbsentPut() también es segura para subprocesos y atómica.

import org.eclipse.collections.impl.map.mutable.ConcurrentHashMap; ... ConcurrentHashMap<String, MyObject> map = new ConcurrentHashMap<>(); map.getIfAbsentPut("key", () -> someExpensiveComputation());

La implementación de org.eclipse.collections.impl.map.mutable.ConcurrentHashMap es verdaderamente no-bloqueante. Si bien se hacen todos los esfuerzos posibles para no invocar innecesariamente la función de fábrica, aún existe la posibilidad de que se la llame más de una vez durante la disputa.

Este hecho lo diferencia del ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>) de Java 8 ConcurrentHashMap.computeIfAbsent(K, Function<? super K,? extends V>) . El Javadoc para este método establece:

La invocación completa del método se realiza atómicamente, por lo que la función se aplica como máximo una vez por clave. Algunas operaciones de actualización intentadas en este mapa por otros hilos pueden bloquearse mientras el cálculo está en progreso, por lo que el cálculo debe ser breve y simple ...

Nota: soy un committer para las colecciones de Eclipse.