java concurrency synchronization double-checked-locking

java - Bloqueo comprobado doble con ConcurrentMap



concurrency synchronization (6)

¿Por qué no utilizar el método putIfAbsent () en ConcurrentMap?

if(!map.containsKey(key)){ map.putIfAbsent(key, getResource(key)); }

Es concebible que pueda llamar a getResource () más de una vez, pero no va a suceder un montón de veces. El código más simple es menos probable que te muerda.

Tengo un fragmento de código que pueden ejecutar varios hilos que necesitan realizar una operación de E / S para inicializar un recurso compartido que está almacenado en un ConcurrentMap . Necesito hacer que este código sea seguro y evitar llamadas innecesarias para inicializar el recurso compartido. Aquí está el código buggy:

private ConcurrentMap<String, Resource> map; // ..... String key = "somekey"; Resource resource; if (map.containsKey(key)) { resource = map.get(key); } else { resource = getResource(key); // I/O-bound, expensive operation map.put(key, resource); }

Con el código anterior, varios hilos pueden verificar el ConcurrentMap y ver que el recurso no está allí, y todos intentan llamar a getResource() que es caro. Para garantizar solo una inicialización única del recurso compartido y hacer que el código sea eficiente una vez que el recurso se ha inicializado, quiero hacer algo como esto:

String key = "somekey"; Resource resource; if (!map.containsKey(key)) { synchronized (map) { if (!map.containsKey(key)) { resource = getResource(key); map.put(key, resource); } } }

¿Es esta una versión segura de doble bloqueo comprobado? Me parece que, dado que los controles se ConcurrentMap en ConcurrentMap , se comporta como un recurso compartido que se declara volatile y, por lo tanto, evita cualquier problema de "inicialización parcial" que pueda ocurrir.


El veredicto está listo. Temporicé 3 soluciones diferentes en precisión de nanosegundos, ya que después de toda la pregunta inicial era sobre el rendimiento:

Completamente sincronizando la función en un HashMap regular :

synchronized (map) { Object result = map.get(key); if (result == null) { result = new Object(); map.put(key, result); } return result; }

primera invocación: 15,000 nanosegundos, invocaciones posteriores: 700 nanosegundos

Usando el bloqueo de doble comprobación con un ConcurrentHashMap :

if (!map.containsKey(key)) { synchronized (map) { if (!map.containsKey(key)) { map.put(key, new Object()); } } } return map.get(key);

primera invocación: 15,000 nanosegundos, invocaciones posteriores: 1500 nanosegundos

Un sabor diferente de ConcurrentHashMap con doble verificación :

Object result = map.get(key); if (result == null) { synchronized (map) { if (!map.containsKey(key)) { result = new Object(); map.put(key, result); } else { result = map.get(key); } } } return result;

primera invocación: 15,000 nanosegundos, invocaciones posteriores: 1000 nanosegundos

Puede ver que el mayor costo fue en la primera invocación, pero fue similar para todos 3. Las invocaciones subsiguientes fueron las más rápidas en el HashMap regular con sincronización de método como se sugirió user237815 pero solo en 300 segundos de NANO. Y, después de todo, estamos hablando de NANO segundos aquí, lo que significa un MILLÓN DE MILES de segundo.


En general, el bloqueo con doble verificación es seguro si la variable en la que se está sincronizando está marcada como volátil. Pero es mejor sincronizar toda la función:

public synchronized Resource getResource(String key) { Resource resource = map.get(key); if (resource == null) { resource = expensiveGetResourceOperation(key); map.put(key, resource); } return resource; }

El golpe de rendimiento será pequeño y estarás seguro de que no habrá problemas de sincronización.

Editar:

En realidad, esto es más rápido que las alternativas, porque en la mayoría de los casos no tendrá que hacer dos llamadas al mapa. La única operación adicional es la verificación nula, y el costo de eso es cercano a cero.

Segunda edición:

Además, no tiene que usar ConcurrentMap. Un HashMap regular lo hará. Más rápido todavía.


No es necesario, ConcurrentMap lo admite como con su método especial putIfAbsent atómico.

No reinventar la rueda: siempre use la API siempre que sea posible.


Si puede utilizar bibliotecas externas, eche un vistazo a MapMaker.makeComputingMap () de Guava. Está hecho a medida para lo que estás tratando de hacer.


sí es ''seguro.

Si map.containsKey(key) es verdadero, según doc, map.put(key, resource) sucede antes que él. Por getResource(key) tanto, getResource(key) pasa antes que resource = map.get(key) , todo está sano y salvo.