44

De Cabeza En primer libro de patrones de diseño, el patrón Singleton con bloqueo doble comprobación se ha implementado de la siguiente manera:¿Por qué es volátil utilizado en este ejemplo de una doble comprobación de bloqueo

public class Singleton { 
    private volatile static Singleton instance; 
    private Singleton() {} 
    public static Singleton getInstance() { 
     if (instance == null) { 
      synchronized (Singleton.class) { 
       if (instance == null) { 
        instance = new Singleton(); 
       } 
      } 
     } 
     return instance; 
    } 
} 

no entiendo por qué es ser volatile usado. ¿El uso de volatile no derrota el propósito del uso de doble bloqueo comprobado, es decir, el rendimiento?

+4

Pensé que el doble bloqueo comprobado estaba roto, ¿alguien lo solucionó? –

+4

Por lo que vale, descubrí que los patrones de diseño de Head First son un libro horrible del que aprender. Cuando miro hacia atrás, tiene mucho sentido ahora que aprendí los patrones en otra parte, pero aprender sin conocer los patrones realmente no sirvió para su propósito. Pero es muy popular, así que tal vez solo era yo siendo denso. :-) – corsiKa

+0

@DavidHeffernan He visto este ejemplo utilizado como la única manera en que se puede confiar en que el jvm haga el DCL. –

Respuesta

47

Se necesita un buen recurso para comprender por qué se necesita volatile del libro . Wikipedia tiene un decent explanation de ese material también.

El problema real es que Thread A puede asignar un espacio de memoria para instance antes de que termine de construir instance. Thread B verá esa asignación e intentará usarla. Esto da como resultado Thread B que falla porque está utilizando una versión parcialmente construida de instance.

+0

Bien, parece una nueva implementación de problemas de memoria volátiles fijos con DCL. Lo que aún no entiendo es la implicación en el rendimiento de usar volátiles aquí. Por lo que he leído, volátil es casi tan lento como sincronizado, ¿por qué no simplemente sincronizar toda la llamada al método getInstance()? – toc777

+4

@ toc777 'volátil' es más lento de lo habitual. Si busca rendimiento, busque un patrón de clase de titular. 'volátil' está aquí simplemente para mostrar que * hay una forma * de hacer que el patrón roto funcione. Es más un desafío de codificación que un problema real. – alf

+0

@alf explica mucho, gracias. No lo dejaron en claro en el libro. – toc777

0

Si no lo tiene, un segundo subproceso podría entrar en el bloque sincronizado después de que el primero lo estableciera en nulo, y su caché local todavía pensaría que era nulo.

El primero no es para ser correcto (si fuera correcto, sería autodestructivo) sino para optimizarlo.

1

Declarar la variable como volatile garantiza que todos los accesos realmente lean su valor actual de la memoria.

Sin volatile, el compilador puede optimizar los accesos a la memoria y mantener su valor en un registro, por lo que solo el primer uso de la variable lee la ubicación real de la memoria que contiene la variable. Esto es un problema si la variable es modificada por otro hilo entre el primer y el segundo acceso; el primer subproceso solo tiene una copia del primer valor (pre-modificado), por lo que la segunda instrucción if prueba una copia obsoleta del valor de la variable.

+3

-1 Estoy perdiendo mis puntos de reputación hoy :) La razón real es que hay memoria caché, modelada como memoria local de subprocesos.El orden en que la memoria local se enjuaga con el principal no está definido, es decir, a menos que tenga * relaciones * pasa * antes *, por ejemplo, al usar 'volátil '. Los registros no tienen nada que ver con los objetos incompletos y el problema de DCL. – alf

+3

Su definición de 'volátil 'es demasiado estrecha; si eso fuera todo volátil, el bloqueo doblemente comprobado hubiera funcionado bien en Voo

+1

@TimBender if singleton _contains_ mutable state, flushing no tiene nada que ver con una referencia al singleton en sí (bueno, hay un enlace indirecto, como el acceso a una referencia 'volatlie' a un singleton hace que el hilo vuelva a leer la memoria principal, pero es un efecto secundario, no la causa de un problema :)) – alf

4

Bueno, no hay doble bloqueo verificado para el rendimiento. Es un roto patrón.

emociones Dejando de lado, volatile está aquí porque sin ella por el tiempo pasa instance == null segundo hilo, primer hilo podría no construye new Singleton() aún: nadie se promete que la creación del objeto sucede antes asignación a instance para cualquier hilo, pero el que realmente crea el objeto.

volatile a su vez establece sucede antes de relación entre lecturas y escrituras, y corrige el patrón roto.

Si está buscando rendimiento, utilice la clase estática interna del soporte en su lugar.

+0

Solucionado, gracias @Kapol! – alf

1

Una lectura volátil no es realmente cara en sí misma.

Puede diseñar una prueba para llamar al getInstance() en un ciclo cerrado, para observar el impacto de una lectura volátil; sin embargo, esa prueba no es realista; En tal situación, el programador normalmente llama al getInstance() una vez y almacena en caché la instancia por el tiempo de uso.

Otra impl es mediante el uso de un campo final (ver wikipedia). Esto requiere una lectura adicional, que puede ser más costosa que la versión volatile. La versión final puede ser más rápida en un circuito cerrado, sin embargo, esa prueba es discutible como se argumentó anteriormente.

9

Según lo citado por @irreputable, volátil no es caro. Incluso si es costoso, la consistencia debe tener prioridad sobre el rendimiento.

Hay una manera más limpia y elegante para Lazy Singletons.

public final class Singleton { 
    private Singleton() {} 
    public static Singleton getInstance() { 
     return LazyHolder.INSTANCE; 
    } 
    private static class LazyHolder { 
     private static final Singleton INSTANCE = new Singleton(); 
    } 
} 

Fuente del artículo: Initialization-on-demand_holder_idiom de Wikipedia

En la ingeniería de software, el titular de inicialización en la demanda (patrón de diseño) modismo es un producto único, cargado ligeramente. En todas las versiones de Java, el lenguaje permite una inicialización perezosa segura, altamente concurrente con buen rendimiento

Puesto que la clase no tiene ningún static variables para inicializar, la inicialización completa trivial.

La definición de clase estática LazyHolder dentro de ella no se inicializa hasta que la JVM determine que se debe ejecutar LazyHolder.

La clase estática LazyHolder sólo se ejecuta cuando el método estático getInstance se invoca en la clase Singleton, y la primera vez que esto sucede la JVM cargar e inicializar la clase LazyHolder.

Esta solución es segura para subprocesos sin requerir construcciones de lenguaje especiales (es decir, volatile o synchronized).

Cuestiones relacionadas