thread synchronizedmap safe current java multithreading concurrenthashmap

synchronizedmap - java hashmap thread safe



¿Se garantiza que ConcurrentHashMap.get() vea un ConcurrentHashMap.put() anterior por un hilo diferente? (8)

¿Se garantiza que ConcurrentHashMap.get() vea un ConcurrentHashMap.put() por un hilo diferente? Mi expectativa es que es y la lectura de los JavaDocs parece indicar eso, pero estoy 99% convencido de que la realidad es diferente. En mi servidor de producción parece estar sucediendo lo siguiente. (Lo he atrapado con el registro.)

Ejemplo de pseudo código:

static final ConcurrentHashMap map = new ConcurrentHashMap(); //sharedLock is key specific. One map, many keys. There is a 1:1 // relationship between key and Foo instance. void doSomething(Semaphore sharedLock) { boolean haveLock = sharedLock.tryAcquire(3000, MILLISECONDS); if (haveLock) { log("Have lock: " + threadId); Foo foo = map.get("key"); log("foo=" + foo); if (foo == null) { log("New foo time! " + threadId); foo = new Foo(); //foo is expensive to instance map.put("key", foo); } else log("Found foo:" + threadId); log("foo=" + foo); sharedLock.release(); } else log("No lock acquired"); }

Lo que parece estar sucediendo es esto:

Thread 1 Thread 2 - request lock - request lock - have lock - blocked waiting for lock - get from map, nothing there - create new foo - place new foo in map - logs foo.toString() - release lock - exit method - have lock - get from map, NOTHING THERE!!! (Why not?) - create new foo - place new foo in map - logs foo.toString() - release lock - exit method

Por lo tanto, mi salida se ve así:

Have lock: 1 foo=null New foo time! 1 foo=foo@cafebabe420 Have lock: 2 foo=null New foo time! 2 foo=foo@boof00boo

El segundo hilo no ve de inmediato el puesto! ¿Por qué? En mi sistema de producción, hay más subprocesos y solo he visto un subproceso, el primero que sigue inmediatamente al subproceso 1, tiene un problema.

Incluso he intentado reducir el nivel de concurrencia en ConcurrentHashMap a 1, no es que importe. P.ej:

static ConcurrentHashMap map = new ConcurrentHashMap(32, 1);

¿A dónde me voy mal? ¿Mi expectativa? ¿O hay algún error en mi código (el software real, no el anterior) que está causando esto? Lo revisé repetidamente y estoy 99% seguro de que estoy manejando el bloqueo correctamente. Ni siquiera puedo entender un error en ConcurrentHashMap o JVM. Por favor, sálvame de mí mismo.

Específicos de Gorey que podrían ser relevantes:

  • Xeon de 64 bits de cuatro núcleos (DL380 G5)
  • RHEL4 ( Linux mysvr 2.6.9-78.0.5.ELsmp #1 SMP ... x86_64 GNU/Linux )
  • Java 6 ( build 1.6.0_07-b06 , 64-Bit Server VM (build 10.0-b23, mixed mode) )

¿Estamos viendo una manifestación interesante del modelo de memoria de Java? ¿Bajo qué condiciones se guardan los registros en la memoria principal? Creo que está garantizado que si dos subprocesos se sincronizan en el mismo objeto, verán una vista de memoria consistente.

No sé lo que Semphore hace internamente, casi obviamente debe hacer algo de sincronización, pero ¿lo sabemos?

Que pasa si tu lo haces

synchronize(dedicatedLockObject)

En lugar de adquirir el semáforo?


¿Por qué estás bloqueando un mapa hash concurrente? Por def. Su hilo seguro. Si hay un problema, está en su código de bloqueo. Es por eso que tenemos paquetes seguros para subprocesos en Java. La mejor manera de depurar esto es con la sincronización de barrera.


Algunas buenas respuestas aquí, pero por lo que puedo decir, nadie ha proporcionado una respuesta canónica a la pregunta: "Es ConcurrentHashMap.get () garantizado para ver un ConcurrentHashMap.put () anterior por un hilo diferente". Los que han dicho que sí no han proporcionado una fuente.

Entonces: sí, está garantizado. Source (ver la sección ''Propiedades de consistencia de memoria''):

Las acciones en un subproceso antes de colocar un objeto en cualquier colección concurrente ocurren antes de las acciones posteriores al acceso o eliminación de ese elemento de la colección en otro subproceso.


Este problema de crear un objeto costoso de crear en un caché basado en una falla para encontrarlo en el caché es un problema conocido. Y afortunadamente esto ya se había implementado.

Puedes usar MapMaker desde Google Collecitons . Simplemente le da una devolución de llamada que crea su objeto, y si el código del cliente se ve en el mapa y el mapa está vacío, se llama a la devolución de llamada y el resultado se coloca en el mapa.

Ver MapMaker ...

ConcurrentMap<Key, Graph> graphs = new MapMaker() .concurrencyLevel(32) .softKeys() .weakValues() .expiration(30, TimeUnit.MINUTES) .makeComputingMap( new Function<Key, Graph>() { public Graph apply(Key key) { return createExpensiveGraph(key); } });

Por cierto, en su ejemplo original no hay ninguna ventaja al usar un Mapa de Hash Concurrente, ya que está bloqueando cada acceso, ¿por qué no usar un HashMap normal dentro de su sección bloqueada?


No creo que el problema esté en el "Mapa de Hash Concurrente", sino en algún lugar de su código o sobre el razonamiento de su código. No puedo detectar el error en el código de arriba (tal vez simplemente no vemos la parte mala?).

Pero para responder a su pregunta "¿Se garantiza que ConcurrentHashMap.get () vea un ConcurrentHashMap.put () anterior por un hilo diferente?" He hackeado juntos un pequeño programa de prueba.

En resumen: No, ¡ConcurrentHashMap está bien!

Si el mapa está mal escrito, el siguiente programa debería imprimir "¡Acceso incorrecto!" Al menos de vez en cuando. Lanza 100 hilos con 100000 llamadas al método descrito anteriormente. Pero imprime "¡Todo bien!".

import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class Test { private final static ConcurrentHashMap<String, Test> map = new ConcurrentHashMap<String, Test>(); private final static Semaphore lock = new Semaphore(1); private static int counter = 0; public static void main(String[] args) throws InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(100); List<Callable<Boolean>> testCalls = new ArrayList<Callable<Boolean>>(); for (int n = 0; n < 100000; n++) testCalls.add(new Callable<Boolean>() { @Override public Boolean call() throws Exception { doSomething(lock); return true; } }); pool.invokeAll(testCalls); pool.shutdown(); pool.awaitTermination(5, TimeUnit.SECONDS); System.out.println("All ok!"); } static void doSomething(Semaphore lock) throws InterruptedException { boolean haveLock = lock.tryAcquire(3000, TimeUnit.MILLISECONDS); if (haveLock) { Test foo = map.get("key"); if (foo == null) { foo = new Test(); map.put("key", new Test()); if (counter > 0) System.err.println("Bad access!"); counter++; } lock.release(); } else { System.err.println("Fail to lock!"); } } }


Si un subproceso coloca un valor en un mapa hash concurrente, se garantiza que otro subproceso que recupera el valor del mapa ve los valores insertados por el subproceso anterior.

Este problema se ha aclarado en "Java Concurrency in Practice" de Joshua Bloch.

Citando del texto:

Las colecciones de bibliotecas seguras para subprocesos ofrecen las siguientes garantías de publicación seguras, incluso si el javadoc es menos claro en el tema:

  • La colocación de una clave o valor en un Hashtable , synchronizedMap o Concurrent-Map publica de forma segura en cualquier otro hilo que lo recupere del Mapa (ya sea directamente o mediante un iterador);

Una cosa a considerar, es si sus claves son iguales y tienen códigos hash idénticos en ambos momentos de la llamada "obtener". Si solo son String , entonces sí, no habrá ningún problema aquí. Pero como no ha dado el tipo genérico de las claves y ha omitido los detalles "no importantes" en el pseudocódigo, me pregunto si está usando otra clase como clave.

En cualquier caso, es posible que desee registrar adicionalmente el código hash de las claves utilizadas para obtener / colocar en los subprocesos 1 y 2. Si son diferentes, tiene su problema. También tenga en cuenta que key1.equals(key2) debe ser verdadero; esto no es algo que se pueda registrar definitivamente, pero si las claves no son clases finales, valdría la pena registrar su nombre de clase completo, y luego mirar el método equals () para esa clase / clases para ver si es posible que el La segunda clave podría considerarse desigual a la primera.

Y para responder a su título, sí, se garantiza que ConcurrentHashMap.get () verá cualquier put anterior (), donde "anterior" significa que hay una relación de suceso antes entre las dos como lo especifica el modelo de memoria Java. (Para ConcurrentHashMap en particular, esto es esencialmente lo que cabría esperar, con la advertencia de que es posible que no pueda saber qué sucede primero si ambos subprocesos se ejecutan "exactamente al mismo tiempo" en diferentes núcleos. En su caso, sin embargo , definitivamente deberías ver el resultado del put () en el hilo 2).


Actualización: putIfAbsent() es lógicamente correcto aquí, pero no evita el problema de solo crear un Foo en el caso de que la clave no esté presente. Siempre crea el Foo, incluso si no termina por ponerlo en el mapa. La respuesta de David Roussel es buena, suponiendo que pueda aceptar la dependencia de Google Collections en su aplicación.

Tal vez me esté perdiendo algo obvio, pero ¿por qué estás protegiendo el mapa con un Semáforo? ConcurrentHashMap (CHM) es seguro para la ejecución de subprocesos (suponiendo que se publique de forma segura, que está aquí). Si está tratando de obtener atómica "poner si no está ya allí", use chm. putIfAbsent() . Si necesita más invariantes complementarios en los que el contenido del mapa no pueda cambiar, probablemente necesite usar un HashMap regular y sincronizarlo de la manera habitual.

Para responder a su pregunta de forma más directa: una vez que devuelva, el valor que coloque en el mapa se verá en el siguiente hilo que lo busque.

Nota al margen, solo un +1 a algunos otros comentarios acerca de poner el lanzamiento del semáforo en un final.

if (sem.tryAcquire(3000, TimeUnit.MILLISECONDS)) { try { // do stuff while holding permit } finally { sem.release(); } }