Bloqueo controlado doble Java

Me encontré con un artículo que discutió recientemente sobre el patrón de locking doblemente verificado en Java y sus peligros, y ahora me pregunto si una variante de ese patrón que he estado usando durante años está sujeta a cualquier problema.

He consultado muchas publicaciones y artículos sobre el tema y entiendo los posibles problemas para obtener una referencia a un objeto parcialmente construido, y hasta donde puedo decir, no creo que mi implementación esté sujeta a estos problemas. ¿Hay algún problema con el siguiente patrón?

Y, si no, ¿por qué la gente no lo usa? Nunca lo he visto recomendado en ninguna de las discusiones que he visto sobre este tema.

public class Test { private static Test instance; private static boolean initialized = false; public static Test getInstance() { if (!initialized) { synchronized (Test.class) { if (!initialized) { instance = new Test(); initialized = true; } } } return instance; } } 

El locking de doble cheque está roto . Dado que Initialized es una primitiva, puede no requerir que sea volátil para funcionar, sin embargo, nada impide que la inicialización se vea como verdadera para el código no sincronizado antes de que se inicialice la instancia.

EDITAR: para aclarar la respuesta anterior, se formuló la pregunta original sobre el uso de un booleano para controlar el locking de doble comprobación. Sin las soluciones en el enlace de arriba, no funcionará. Puede verificar el locking en realidad configurando un valor booleano, pero aún tiene problemas con el reordenamiento de instrucciones cuando se trata de crear la instancia de clase. La solución sugerida no funciona porque es posible que la instancia no se inicialice después de ver el booleano inicializado como verdadero en el bloque no sincronizado.

La solución adecuada para verificar dos veces el locking es utilizar volátil (en el campo de instancia) y olvidarse del booleano inicializado, y asegúrese de utilizar JDK 1.5 o superior, o inicializarlo en un campo final, como se detalla en el enlace artículo y la respuesta de Tom, o simplemente no lo use.

Ciertamente, todo el concepto parece una gran optimización prematura, a menos que sepa que va a obtener una gran cantidad de conflictos al obtener este Singleton, o ha perfilado la aplicación y ha visto que este es un punto caliente.

Eso funcionaría si initialized fuera volatile . Al igual que con los efectos synchronized de volatile synchronized realmente no se trata tanto de la referencia como de lo que podemos decir sobre otros datos. La configuración del campo de la instance y el objeto de Test es forzada a suceder, antes de la escritura initialized . Cuando se utiliza el valor en caché a través del cortocircuito, se produce la lectura de initialize , antes de leer la instance y los objetos alcanzados a través de la referencia. No hay una diferencia significativa en tener una bandera initialized separado (aparte de que causa aún más complejidad en el código).

(Las reglas para los campos final en constructores para publicación insegura son un poco diferentes).

Sin embargo, rara vez debería ver el error en este caso. Las posibilidades de meterse en problemas al usarlas por primera vez son mínimas, y es una carrera no repetida.

El código es demasiado complicado. Podrías simplemente escribirlo como:

 private static final Test instance = new Test(); public static Test getInstance() { return instance; } 

El locking doblemente verificado se ha roto, y la solución al problema es en realidad más sencilla de implementar en código que este modismo: simplemente use un inicializador estático.

 public class Test { private static final Test instance = createInstance(); private static Test createInstance() { // construction logic goes here... return new Test(); } public static Test getInstance() { return instance; } } 

Se garantiza que se ejecutará un inicializador estático la primera vez que la JVM carga la clase, y antes de que la referencia de la clase se pueda devolver a cualquier subproceso, lo que la hace intrínsecamente segura para el hilo.

Esta es la razón por la cual el locking verificado doble está roto.

Sincronice las garantías, que solo un hilo puede ingresar a un bloque de código. Pero no garantiza que las modificaciones de variables hechas dentro de la sección sincronizada serán visibles para otros hilos. Solo los hilos que entran en el bloque sincronizado están garantizados para ver los cambios. Esta es la razón por la cual el locking verificado doble está roto: no está sincronizado por el lado del lector. El hilo de lectura puede ver que el singleton no es nulo, pero los datos singleton pueden no estar totalmente inicializados (visibles).

El pedido es provisto por volatile . volatile pedido de garantías volatile , por ejemplo, escribir en campo estático de singleton volátil garantiza que las escrituras en el objeto singleton finalizarán antes de escribir en el campo estático volátil. No impide la creación de una sola fila de dos objetos, esto es provisto por synchronize.

Los campos estáticos finales de clase no necesitan ser volátiles. En Java, la JVM se ocupa de este problema.

Ver mi publicación, una respuesta al patrón de Singleton y al locking comprobado doble roto en una aplicación Java del mundo real , que ilustra un ejemplo de un singleton con respecto al locking comprobado doble que parece ingenioso pero está roto.

Probablemente deberías usar los tipos de datos atómicos en java.util.concurrent.atomic .

Si “inicializado” es verdadero, entonces “instancia” DEBE estar completamente inicializado, lo mismo que 1 más 1 es igual a 2 :). Por lo tanto, el código es correcto. La instancia solo se instancia una vez, pero la función se puede llamar un millón de veces, por lo que mejora el rendimiento sin verificar la sincronización de un millón menos una vez.

Todavía hay algunos casos en que se puede usar una doble verificación.

  1. Primero, si realmente no necesita un singleton, y la doble verificación se usa solo para NO crear e inicializar a muchos objetos.
  2. Hay un campo final establecido al final del constructor / bloque inicializado (que hace que todos los campos inicializados previamente sean vistos por otros hilos).

He estado investigando sobre el idioma de locking comprobado doble y por lo que entendí, su código podría llevar al problema de leer una instancia parcialmente construida A MENOS QUE su clase de prueba sea inmutable:

El modelo de memoria de Java ofrece una garantía especial de seguridad de inicialización para compartir objetos inmutables.

Se puede acceder de forma segura incluso cuando la sincronización no se utiliza para publicar la referencia del objeto.

(Citas del muy recomendable libro Java Concurrency in Practice)

Entonces, en ese caso, la expresión idiomática de locking doble funcionaría.

Pero, si ese no es el caso, observe que está devolviendo la instancia variable sin sincronización, por lo que la variable de instancia puede no estar completamente construida (vería los valores predeterminados de los atributos en lugar de los valores provistos en el constructor).

La variable booleana no agrega nada para evitar el problema, ya que puede establecerse en verdadero antes de que se inicialice la clase de prueba (la palabra clave sincronizada no evita el reordenamiento completo, algunas dependencias pueden cambiar el orden). No hay regla de sucede antes en el Modelo de memoria de Java para garantizar eso.

Y hacer que boolean sea volátil tampoco agregaría nada, porque las variables de 32 bits se crean atómicamente en Java. La expresión idiomática de locking doble también funcionaría con ellos.

Desde Java 5, puede solucionar ese problema declarando la variable de instancia como volátil.

Puede leer más sobre el idioma doblemente verificado en este artículo muy interesante .

Finalmente, algunas recomendaciones que he leído:

  • Considere si debe usar el patrón singleton. Muchas personas lo consideran un antipatrón. Se prefiere la dependency injection siempre que sea posible. Mira esto

  • Considere cuidadosamente si la optimización de doble locking revisado es realmente necesaria antes de implementarla, porque en la mayoría de los casos, eso no valdría la pena el esfuerzo. Además, considere la posibilidad de construir la clase de prueba en el campo estático, porque la carga diferida solo es útil cuando la construcción de una clase requiere una gran cantidad de recursos y en la mayoría de los casos, no es el caso.

Si aún necesita realizar esta optimización, consulte este enlace que proporciona algunas alternativas para lograr un efecto similar al que está intentando.

El problema DCL está roto, a pesar de que parece funcionar en muchas máquinas virtuales. Hay un buen informe sobre el problema aquí http://www.javaworld.com/article/2075306/java-concurrency/can-double-checked-locking-be-fixed-.html .

multiproceso y coherencia de memoria son temas más complicados de lo que podrían parecer. […] Puede ignorar toda esta complejidad si solo usa la herramienta que proporciona Java para este propósito: la sincronización. Si sincroniza cada acceso a una variable que podría haber sido escrita, o podría ser leída por, otro hilo, no tendrá problemas de coherencia de memoria.

La única forma de resolver este problema correctamente es evitar la inicialización lenta (hazlo con entusiasmo) o realizar una sola comprobación dentro de un bloque sincronizado. El uso del booleano initialized es equivalente a una verificación nula de la referencia en sí misma. Un segundo subproceso puede ver que initialized es verdadero, pero la instance aún puede ser nula o parcialmente inicializada.

Doble control El locking es el antipatrón.

La clase de titular de inicialización diferida es el patrón que debería observar.

A pesar de tantas otras respuestas, pensé que debía responder porque todavía no hay una respuesta simple que diga por qué DCL se rompe en muchos contextos, por qué es innecesario y qué debe hacer en su lugar. Así que utilizaré una cita de Goetz: Java Concurrency In Practice que para mí proporciona la explicación más sucinta en su capítulo final sobre el Modelo de memoria de Java.

Se trata de la publicación segura de variables:

El problema real con DCL es la suposición de que lo peor que puede suceder al leer una referencia de objeto compartido sin sincronización es ver erróneamente un valor obsoleto (en este caso, nulo); en ese caso, la expresión DCL compensa este riesgo al intentar nuevamente con la cerradura retenida. Pero el peor de los casos es considerablemente peor: es posible ver un valor actual de la referencia pero valores obsoletos para el estado del objeto, lo que significa que el objeto podría verse en un estado no válido o incorrecto.

Los cambios posteriores en el JMM (Java 5.0 y posterior) han permitido que DCL funcione si el recurso se vuelve volátil, y el impacto en el rendimiento de esto es pequeño ya que las lecturas volátiles suelen ser solo un poco más caras que las lecturas no volátiles.

Sin embargo, este es un modismo cuya utilidad ha pasado en gran medida: las fuerzas que lo motivaron (sincronización lenta sin control, arranque lento de JVM) ya no están en juego, lo que lo hace menos efectivo como optimización. La expresión idiomática del titular de inicialización perezosa ofrece los mismos beneficios y es más fácil de entender.

Listado 16.6. Lazy Initialization Holder Class Idiom.

 public class ResourceFactory private static class ResourceHolder { public static Resource resource = new Resource(); } public static Resource getResource() { return ResourceHolder.resource; } } 

Esa es la manera de hacerlo.

Primero, para singletons puede usar un Enum, como se explica en esta pregunta Implementando Singleton con un Enum (en Java)

En segundo lugar, desde Java 1.5, puede usar una variable volátil con doble locking comprobado, como se explica al final de este artículo: https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

    Intereting Posts