java multithreading final java-memory-model double-checked-locking

java - Cerradura de doble control sin volatilidad.



multithreading final (4)

Leí esta pregunta sobre cómo hacer el bloqueo de doble comprobación:

// Double-check idiom for lazy initialization of instance fields private volatile FieldType field; FieldType getField() { FieldType result = field; if (result == null) { // First check (no locking) synchronized(this) { result = field; if (result == null) // Second check (with locking) field = result = computeFieldValue(); } } return result; }

Mi objetivo es conseguir que el trabajo de carga lenta en un campo (NO un singleton) sin el atributo volátil. El objeto de campo nunca se cambia después de la inicialización.

Después de algunas pruebas mi enfoque final:

private FieldType field; FieldType getField() { if (field == null) { synchronized(this) { if (field == null) field = Publisher.publish(computeFieldValue()); } } return fieldHolder.field; } public class Publisher { public static <T> T publish(T val){ return new Publish<T>(val).get(); } private static class Publish<T>{ private final T val; public Publish(T val) { this.val = val; } public T get(){ return val; } } }

El beneficio es posiblemente un tiempo de acceso más rápido debido a que no necesita volátiles, al mismo tiempo que mantiene la simplicidad con la clase de Publisher reutilizable.

He probado esto utilizando jcstress. SafeDCLFinal funcionó como se esperaba, mientras que UnsafeDCLFinal fue inconsistente (como se esperaba). En este punto estoy 99% seguro de que funciona, pero por favor, demuéstrame que estoy equivocado. Compilado con mvn clean install -pl tests-custom -am y ejecutado con java -XX:-UseCompressedOops -jar tests-custom/target/jcstress.jar -t DCLFinal . Código de prueba a continuación (en su mayoría, clases de prueba de singleton modificadas)

/* * SafeDCLFinal.java: */ package org.openjdk.jcstress.tests.singletons; public class SafeDCLFinal { @JCStressTest @JCStressMeta(GradingSafe.class) public static class Unsafe { @Actor public final void actor1(SafeDCLFinalFactory s) { s.getInstance(SingletonUnsafe::new); } @Actor public final void actor2(SafeDCLFinalFactory s, IntResult1 r) { r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new)); } } @JCStressTest @JCStressMeta(GradingSafe.class) public static class Safe { @Actor public final void actor1(SafeDCLFinalFactory s) { s.getInstance(SingletonSafe::new); } @Actor public final void actor2(SafeDCLFinalFactory s, IntResult1 r) { r.r1 = Singleton.map(s.getInstance(SingletonSafe::new)); } } @State public static class SafeDCLFinalFactory { private Singleton instance; // specifically non-volatile public Singleton getInstance(Supplier<Singleton> s) { if (instance == null) { synchronized (this) { if (instance == null) { // instance = s.get(); instance = Publisher.publish(s.get(), true); } } } return instance; } } } /* * UnsafeDCLFinal.java: */ package org.openjdk.jcstress.tests.singletons; public class UnsafeDCLFinal { @JCStressTest @JCStressMeta(GradingUnsafe.class) public static class Unsafe { @Actor public final void actor1(UnsafeDCLFinalFactory s) { s.getInstance(SingletonUnsafe::new); } @Actor public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) { r.r1 = Singleton.map(s.getInstance(SingletonUnsafe::new)); } } @JCStressTest @JCStressMeta(GradingUnsafe.class) public static class Safe { @Actor public final void actor1(UnsafeDCLFinalFactory s) { s.getInstance(SingletonSafe::new); } @Actor public final void actor2(UnsafeDCLFinalFactory s, IntResult1 r) { r.r1 = Singleton.map(s.getInstance(SingletonSafe::new)); } } @State public static class UnsafeDCLFinalFactory { private Singleton instance; // specifically non-volatile public Singleton getInstance(Supplier<Singleton> s) { if (instance == null) { synchronized (this) { if (instance == null) { // instance = s.get(); instance = Publisher.publish(s.get(), false); } } } return instance; } } } /* * Publisher.java: */ package org.openjdk.jcstress.tests.singletons; public class Publisher { public static <T> T publish(T val, boolean safe){ if(safe){ return new SafePublish<T>(val).get(); } return new UnsafePublish<T>(val).get(); } private static class UnsafePublish<T>{ T val; public UnsafePublish(T val) { this.val = val; } public T get(){ return val; } } private static class SafePublish<T>{ final T val; public SafePublish(T val) { this.val = val; } public T get(){ return val; } } }

Probado con java 8, pero debería funcionar al menos con java 6+. Ver docs

Pero me pregunto si esto funcionaría:

// Double-check idiom for lazy initialization of instance fields without volatile private FieldHolder fieldHolder = null; private static class FieldHolder{ public final FieldType field; FieldHolder(){ field = computeFieldValue(); } } FieldType getField() { if (fieldHolder == null) { // First check (no locking) synchronized(this) { if (fieldHolder == null) // Second check (with locking) fieldHolder = new FieldHolder(); } } return fieldHolder.field; }

O tal vez incluso:

// Double-check idiom for lazy initialization of instance fields without volatile private FieldType field = null; private static class FieldHolder{ public final FieldType field; FieldHolder(){ field = computeFieldValue(); } } FieldType getField() { if (field == null) { // First check (no locking) synchronized(this) { if (field == null) // Second check (with locking) field = new FieldHolder().field; } } return field; }

O:

// Double-check idiom for lazy initialization of instance fields without volatile private FieldType field = null; FieldType getField() { if (field == null) { // First check (no locking) synchronized(this) { if (field == null) // Second check (with locking) field = new Object(){ public final FieldType field = computeFieldValue(); }.field; } } return field; }

Creo que esto funcionaría basado en este oráculo doc :

El modelo de uso para los campos finales es simple: establece los campos finales para un objeto en el constructor de ese objeto; y no escriba una referencia al objeto que se está construyendo en un lugar donde otro hilo pueda verlo antes de que el constructor del objeto finalice. Si se sigue esto, entonces, cuando el objeto es visto por otro hilo, ese hilo siempre verá la versión correctamente construida de los campos finales de ese objeto . También verá versiones de cualquier objeto o matriz a la que hagan referencia aquellos campos finales que estén al menos tan actualizados como los campos finales.


En breve

La versión del código sin la clase volatile o envoltura depende del modelo de memoria del sistema operativo subyacente en el que se ejecuta la JVM.

La versión con la clase de envoltura es una alternativa conocida conocida como el patrón de diseño del titular de la inicialización a pedido y se basa en el contrato de ClassLoader que cualquier clase dada se carga una vez, al primer acceso y de forma segura para subprocesos.

La necesidad de volatile

La forma en que los desarrolladores piensan en la ejecución del código la mayor parte del tiempo es que el programa se carga en la memoria principal y se ejecuta directamente desde allí. Sin embargo, la realidad es que hay varios cachés de hardware entre la memoria principal y los núcleos del procesador. El problema surge porque cada hilo puede ejecutarse en procesadores separados, cada uno con su propia copia independiente de las variables en el alcance; Si bien nos gusta pensar lógicamente en el field como una ubicación única, la realidad es más complicada.

Para ejecutar un ejemplo simple (aunque quizás detallado), considere un escenario con dos subprocesos y un solo nivel de almacenamiento en caché de hardware, donde cada subproceso tiene su propia copia del field en ese caché. Así que ya hay tres versiones de field : una en la memoria principal, una en la primera copia y una en la segunda copia. Me referiré a estos como field M , field A y field B respectivamente.

  1. Estado inicial
    field M = null
    field A = null
    field B = null
  2. El hilo A realiza la primera comprobación nula, encuentra que el field A es nulo.
  3. El hilo A adquiere el bloqueo de this .
  4. El subproceso B realiza la primera comprobación de nulos, encuentra que el field B es nulo.
  5. El subproceso B intenta obtener el bloqueo en this pero encuentra que está sujeto por el subproceso A. El subproceso B duerme
  6. El hilo A realiza la segunda comprobación nula, encuentra que el field A es nulo.
  7. El hilo A asigna al field A el valor fieldType1 y libera el bloqueo. Como el field no es volatile esta asignación no se propaga.
    field M = null
    field A = fieldType1
    field B = null
  8. El hilo B despierta y adquiere el bloqueo de this .
  9. El subproceso B realiza la segunda comprobación de nulos, encuentra que el field B es nulo.
  10. El hilo B asigna al field B el valor fieldType2 y libera el bloqueo.
    field M = null
    field A = fieldType1
    field B = fieldType2
  11. En algún momento, las escrituras en la copia de caché A se sincronizan de nuevo a la memoria principal.
    field M = fieldType1
    field A = fieldType1
    field B = fieldType2
  12. En algún momento posterior, las escrituras en la copia de caché B se sincronizan de nuevo en la memoria principal, sobrescribiendo la asignación realizada por la copia A.
    field M = fieldType2
    field A = fieldType1
    field B = fieldType2

Como uno de los comentaristas sobre la pregunta mencionada, el uso de volatile asegura que las escrituras sean visibles. No conozco el mecanismo utilizado para garantizar esto; es posible que los cambios se propaguen a cada copia, que las copias nunca se realicen y que todos los accesos de field estén en contra de la memoria principal.

Una última nota sobre esto: mencioné anteriormente que los resultados dependen del sistema. Esto se debe a que diferentes sistemas subyacentes pueden adoptar enfoques menos optimistas para su modelo de memoria y tratar toda la memoria compartida en subprocesos como volatile o tal vez aplicar una heurística para determinar si una referencia en particular debe tratarse como volatile o no, aunque a costa del rendimiento. de sincronizar a memoria principal. Esto puede hacer que la prueba de estos problemas sea una pesadilla; no solo tiene que correr contra una muestra lo suficientemente grande como para intentar desencadenar la condición de carrera, sino que podría estar probando en un sistema que es lo suficientemente conservador como para no desencadenar la condición.

Titularización de inicialización bajo demanda

Lo principal que quería señalar aquí es que esto funciona porque esencialmente estamos introduciendo un singleton en la mezcla. El contrato de ClassLoader significa que, si bien puede haber muchas instancias de Class , solo puede haber una única instancia de Class<A> disponible para cualquier tipo A , que también se carga en la primera vez que se inicia la primera referencia / iniciación. De hecho, puede pensar que cualquier campo estático en la definición de una clase es realmente un campo en un singleton asociado con esa clase en el que se incrementan los privilegios de acceso de miembros entre ese singleton y las instancias de la clase.


Citando la Declaración de "El bloqueo de doble control está roto" mencionada por @Kicsi, la última sección es:

Objetos inmutables de bloqueo de doble control

Si Helper es un objeto inmutable, de modo que todos los campos de Helper son finales, el bloqueo con doble verificación funcionará sin tener que usar campos volátiles . La idea es que una referencia a un objeto inmutable (como una cadena o un entero) se comporte de la misma manera que un int o float; Las referencias de lectura y escritura a objetos inmutables son atómicas.

(el énfasis es mío)

Dado que FieldHolder es inmutable, de hecho no necesita la palabra clave volatile : otros subprocesos siempre verán un FieldHolder correctamente inicializado. Por lo que yo entiendo, FieldType siempre se inicializará antes de poder acceder a él desde otros subprocesos a través de FieldHolder .

Sin embargo, la sincronización adecuada sigue siendo necesaria si FieldType no es inmutable. Por consiguiente, no estoy seguro de que pueda beneficiarse mucho al evitar la palabra clave volatile .

Sin embargo, si es inmutable, entonces no necesita el FieldHolder en absoluto, siguiendo la cita anterior.


Lo primero es lo primero: lo que estás tratando de hacer es, en el mejor de los casos, peligroso. Me estoy poniendo un poco nervioso cuando la gente trata de hacer trampa con las finales. El lenguaje Java le proporciona una herramienta volatile como herramienta para manejar la consistencia entre subprocesos. Utilízalo

De todos modos, el enfoque relevante se describe en "Publicación e inicialización seguras en Java" como:

public class FinalWrapperFactory { private FinalWrapper wrapper; public Singleton get() { FinalWrapper w = wrapper; if (w == null) { // check 1 synchronized(this) { w = wrapper; if (w == null) { // check2 w = new FinalWrapper(new Singleton()); wrapper = w; } } } return w.instance; } private static class FinalWrapper { public final Singleton instance; public FinalWrapper(Singleton instance) { this.instance = instance; } } }

Los términos del laico, funciona así. synchronized produce la sincronización adecuada cuando observamos que la wrapper es nula; en otras palabras, el código sería obviamente correcto si eliminamos la primera comprobación por completo y extendemos la synchronized a todo el cuerpo del método. final en FinalWrapper garantiza que si vimos el wrapper no es nulo, está completamente construido y todos los campos de Singleton son visibles, esto se recupera de la lectura imprecisa del wrapper .

Tenga en cuenta que FinalWrapper el FinalWrapper en el campo, no el valor en sí. Si la instance fuera publicada sin el FinalWrapper , todas las apuestas serían canceladas (en términos simples, esa es una publicación prematura). Esta es la razón por la que su Publisher.publish es disfuncional: simplemente poner el valor en el campo final, leerlo de nuevo y publicarlo de manera insegura no es seguro, es muy similar a simplemente escribir la instance desnuda.

Además, debe tener cuidado de hacer un "repliegue" de lectura bajo el bloqueo, cuando descubra la wrapper nula y use su valor . Hacer la segunda (tercera) lectura de la wrapper en una declaración de devolución también arruinaría la corrección, lo que lo convierte en una raza legítima.

EDIT: Por cierto, todo eso dice que si el objeto que está publicando está cubierto con final -s internamente, puede cortar el intermediario de FinalWrapper y publicar la instance sí.

EDIT 2: Véase también, LCK10-J. Use una forma correcta del lenguaje de bloqueo de doble comprobación y alguna discusión en los comentarios allí.


No, esto no funcionaría.

final no garantiza la visibilidad entre hilos que volatile hace. El documento de Oracle que citó dice que otros subprocesos siempre verán una versión correctamente construida de los campos finales de un objeto. final garantiza que todos los campos finales se hayan construido y establecido para cuando el constructor de objetos haya terminado de ejecutarse. Entonces, si el objeto Foo contiene una bar campo final, se garantiza que la bar se construirá en el momento en que el constructor de Foo haya terminado.

Sin embargo, el objeto al que hace referencia un campo final aún es mutable, y las escrituras en ese objeto pueden no ser visibles correctamente en diferentes subprocesos.

Por lo tanto, en sus ejemplos, no se garantiza que otros subprocesos vean el objeto FieldHolder que se creó y puede crear otro, o si alguna modificación ocurre en el estado del objeto FieldType , no se garantiza que otros subprocesos vean estas modificaciones. La palabra clave final solo garantiza que una vez que los otros subprocesos ven el objeto FieldType , se ha llamado a su constructor.