34

Me encontré con un artículo que discutió recientemente el patrón de doble bloqueo comprobado en Java y sus trampas y ahora me pregunto si una variante de ese patrón que he estado usando durante años está sujeta a cualquier problema .Java doble bloqueo bloqueado

He visto 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 piensa 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; 
    } 
} 
+0

Lo uso. La razón por la que funciona es porque la instancia se instanciará por completo antes de que se toque la línea initialized = true y la asignación de un valor booleano en Java es una operación atómica (en cada chipset que he usado). – Jono

+2

@Jono Depende del compilador y la VM, si funciona o no. El compilador puede producir un bytecode que está cacarando el valor de la expresión '! Initialized', y por lo tanto falta si otro hilo cambia el valor de la propiedad' initialized'. Si el mismo código se ejecuta de forma paralela, la inicialización de la instancia se puede ejecutar dos veces. Además, el compilador JIT puede optimizar esta expresión a partir de un bytecode no optimizado. Solo piense: la VM debe evaluar la misma expresión, inmediatamente después de la otra. Una operación 'dup' impide la reevaluación, y es una transformación de código válida según JLS. – gaborsch

Respuesta

16

que funcionaría si era initializedvolatile. Al igual que con synchronized, los efectos interesantes de volatile no tienen tanto que ver con la referencia como con lo que podemos decir sobre otros datos. La configuración del campo instance y el objeto Test está forzada a suceder -antes de escribir en initialized. Al utilizar el valor almacenado en caché a través del cortocircuito, initialize dice ocurre -antes de lectura de instance y objetos alcanzados a través de la referencia. No hay una diferencia significativa en tener una marca initialized separada (aparte de que causa aún más complejidad en el código).

(Las reglas para final campos de constructores para su publicación inseguros son un poco diferentes.)

Sin embargo, rara vez se debe 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. Simplemente podría escribirlo como:

private static final Test instance = new Test(); 

public static Test getInstance() { 
    return instance; 
} 
+3

Pero ten cuidado de crear el mundo en el inicio de la aplicación. La inicialización lenta tiene sus usos. – rsp

+6

@rsp Esto sigue siendo flojo: mientras no accedas a la clase, no se inicializará. Pero es cierto que, en algún caso, queremos que la pereza sea realmente en el último momento ... – KLE

+0

@Tom - hay un error tipográfico en su valor de retorno – McDowell

23

Double check locking is broken. 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 bloqueo de doble comprobación. Sin las soluciones en el enlace de arriba, no funcionará. Puede verificar el bloqueo 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 bloqueo 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 elaborado en el artículo vinculado y la respuesta de Tom, o simplemente no lo use.

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

+35

El bloqueo comprobado * fue * roto. No olvide leer la sección titulada "Bajo el nuevo modelo de memoria Java" al final de su documento vinculado, que muestra cómo y por qué funciona en los JDK 1.5 y posteriores (es decir, hace más de cinco años). –

+1

@dtsazza, sí, se podía arreglar con volátil ahora, pero el OP no tenía eso, intentaba resolverlo con un booleano. – Yishai

11

El bloqueo de doble comprobación está realmente roto, y la solución al problema es en realidad más simple de implementar en código que este modismo - solo 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; 
    } 
} 

A inicializador estático se garantiza que sea ejecutada la primera vez que la JVM carga la clase, y antes de la referencia de clase pueden ser devueltos a cualquier hilo - por lo que es multi-hilo inherentemente.

+0

Su ejemplo es incorrecto porque * el campo 'instancia' * no está marcado como * 'final' *, por lo tanto, no se garantiza que no haya ningún subproceso que pueda ver * el valor 'nulo' * allí. –

+2

No creo que sea cierto según http://java.sun.com/docs/books/jls/second_edition/html/execution.doc.html#44557 y http://java.sun.com/docs /books/jls/second_edition/html/execution.doc.html#44630 - el modificador final solo parece afectar el orden de los campos que se inicializan. Sin embargo, este campo debe ser final de todos modos en su uso, por lo que he actualizado el código independientemente. –

5

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

Sincronizar 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 bloqueo 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).

Los pedidos son provistos por volatile. volatile garantiza el pedido, por ejemplo, escribir en el 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 de clase final no necesitan ser volátiles. En Java, el JVM se ocupa de este problema.

Ver mi post, an answer to Singleton pattern and broken double checked locking in a real-world Java application, que ilustra un ejemplo de un conjunto unitario con respecto al bloqueo de doble comprobado que parece inteligente, pero está roto.

+0

Si no se garantiza que el cambio se vea en otros hilos, ¿no significa eso que también se rompería la sincronización del método? Pensé que se suponía que era seguro. –

+0

@BillK Hay una cadena de pasar antes de cualquier escritura dentro del bloque sincronizado a cualquier lectura en un bloque sincronizado en el mismo objeto. –

0

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.

0

Todavía hay algunos casos en los 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 provoca que todos los subprocesos que se han inicializado previamente sean vistos por otros subprocesos).
0

he estado investigando acerca de la doble comprobado bloqueo lenguaje y de lo que he entendido, el código podría dar lugar al problema de la lectura de una instancia parcialmente construido a menos que su clase de prueba es inmutable:

El Java Memory Model 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.

(Las citas del libro muy recomendable Java concurrencia en la práctica)

Así que en ese caso, el doble bloqueo comprueban idioma 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 constructor).

La variable booleana no aporta nada para evitar el problema, ya que puede establecerse en true antes de que se inicializa la clase de prueba (la palabra clave sincronizada no evita por completo la reordenación, algunos sencences pueden cambiar el orden). No hay regla de sucede antes en el Modelo de memoria de Java para garantizar eso.

Y hacer que el booleano 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 bloqueo 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 la expresión doble marcada en this very interesting article.

Por último, algunas recomendaciones que he leído:

  • considerar si se debe utilizar el patrón singleton. Muchas personas lo consideran un antipatrón. Se prefiere la inyección de dependencia siempre que sea posible. Compruebe this.

  • Considere cuidadosamente si la optimización de doble bloqueo comprobado 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 todavía es necesario realizar esta optimización, marque esta link que proporciona algunas alternativas para lograr un efecto similar a lo que está tratando.

0

El problema de DCL no funciona, aunque 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 la coherencia de la 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, otra cadena, no tendrá problemas de coherencia de memoria.

La única manera de resolver este problema de manera adecuada es evitar la inicialización perezosa (hacerlo con entusiasmo) o solo cheque 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 hacer que initialized sea verdadero, pero instance aún puede ser nulo o parcialmente inicializado.

0

Doble-comprobado Locking es el anti-patrón.

Lazy Initialization Holder Class es el patrón que debería estar buscando.

A pesar de tantas otras respuestas, pensé que debería responder porque aún 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 usaré una cita del 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 verdadero problema con DCL es el supuesto de que lo peor que puede suceder cuando se lee una referencia de objeto compartido sin sincronización es ver erróneamente un valor rancio (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 la JMM (Java 5.0 y posterior) han permitido DCL a trabajar si los recursos se hace volátil, y el impacto en el rendimiento de esto es pequeño, ya volátil lee son por lo general sólo un poco más caro que lee no volátil.

Sin embargo, este es un idioma cuya utilidad ha fuerzas que motivaron (sincronización lenta uncontended, lento inicio de la JVM) los pasó en gran medida, ya no están en juego, por lo que es menos eficaz como una 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.

Lista 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.