¿Cómo resolver el locking de la statement “locking doble controlado” en Java?

Quiero implementar la inicialización lenta para multihilo en Java.
Tengo un código del tipo:

class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { Helper h; synchronized(this) { h = helper; if (h == null) synchronized (this) { h = new Helper(); } // release inner synchronization lock helper = h; } } return helper; } // other functions and members... } 

Y recibo la statement de “Bloqueo comprobado doble está roto”.
¿Como puedo resolver esto?

Aquí está el modismo recomendado en el Ítem ​​71: Use la inicialización lenta de Juicio Efectivo:

Si necesita utilizar la inicialización diferida para el rendimiento en un campo de instancia, use la expresión de doble verificación . Esta expresión idiomática evita el costo de locking cuando se accede al campo una vez inicializado (Ítem 67). La idea detrás del idioma es verificar el valor del campo dos veces (de ahí el nombre de doble verificación ): una vez sin bloquear, y luego, si el campo parece estar sin inicializar, una segunda vez con locking. Solo si la segunda verificación indica que el campo no está inicializado, la llamada inicializa el campo. Debido a que no hay locking si el campo ya está inicializado, es crítico que el campo se declare volatile (Artículo 66). Aquí está el modismo:

 // 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; } 

Este código puede parecer un poco intrincado. En particular, la necesidad del resultado de la variable local puede ser poco clara. Lo que hace esta variable es asegurarse de que el campo se lea solo una vez en el caso común donde ya se ha inicializado. Aunque no es estrictamente necesario, esto puede mejorar el rendimiento y es más elegante según los estándares aplicados a la progtwigción concurrente de bajo nivel. En mi máquina, el método anterior es aproximadamente un 25 por ciento más rápido que la versión obvia sin una variable local.

Antes de la versión 1.5, el modismo de doble comprobación no funcionaba de manera confiable porque la semántica del modificador volátil no era lo suficientemente fuerte como para soportarlo [Pugh01]. El modelo de memoria presentado en la versión 1.5 solucionó este problema [JLS, 17, Goetz06 16]. En la actualidad, la expresión idiomática doble es la técnica de elección para inicializar perezosamente un campo de instancia. Aunque también puede aplicar el idioma de verificación doble a campos estáticos, no hay ninguna razón para hacerlo: la expresión idiomática de clase de titular de inicialización diferida es una mejor opción.

Referencia

  • Effective Java, segunda edición
    • Elemento 71: use la inicialización lenta de manera juiciosa

Aquí hay un patrón para el locking correcto con doble verificación.

 class Foo { private volatile HeavyWeight lazy; HeavyWeight getLazy() { HeavyWeight tmp = lazy; /* Minimize slow accesses to `volatile` member. */ if (tmp == null) { synchronized (this) { tmp = lazy; if (tmp == null) lazy = tmp = createHeavyWeightObject(); } } return tmp; } } 

Para un singleton, hay una expresión mucho más legible para la inicialización perezosa.

 class Singleton { private static class Ref { static final Singleton instance = new Singleton(); } public static Singleton get() { return Ref.instance; } } 

La única forma de hacer un doble locking comprobado correctamente en Java es usar declaraciones “volátiles” en la variable en cuestión. Si bien esa solución es correcta, tenga en cuenta que “volátil” significa que las líneas de caché se vacían en cada acceso. Como “sincronizados” los vacía al final del bloque, puede que en realidad no sean más eficientes (o incluso menos eficientes). Recomiendo simplemente no usar el locking con doble verificación a menos que hayas perfilado tu código y haya encontrado un problema de rendimiento en esta área.

DCL usando ThreadLocal Por Brian Goetz @ JavaWorld

¿Qué hay de malo en DCL?

DCL se basa en un uso no sincronizado del campo de recursos. Eso parece ser inofensivo, pero no lo es. Para ver por qué, imagine que el hilo A está dentro del bloque sincronizado, ejecutando el enunciado resource = new Resource (); mientras que el hilo B solo está ingresando getResource (). Considere el efecto en la memoria de esta inicialización. La memoria para el nuevo objeto Resource se asignará; se llamará al constructor de Resource, inicializando los campos de miembro del nuevo objeto; y al recurso de campo de SomeClass se le asignará una referencia al objeto recién creado.

 class SomeClass { private Resource resource = null; public Resource getResource() { if (resource == null) { synchronized { if (resource == null) resource = new Resource(); } } return resource; } } 

Sin embargo, dado que el hilo B no se está ejecutando dentro de un bloque sincronizado, puede ver estas operaciones de memoria en un orden diferente al que ejecuta el hilo A. Podría ser el caso que B vea estos eventos en el siguiente orden (y el comstackdor también puede reordenar las instrucciones de esta manera): asignar memoria, asignar referencia al recurso, constructor de llamadas. Supongamos que el hilo B aparece después de que se haya asignado la memoria y se haya establecido el campo de recursos, pero antes de que se invoque el constructor. ¡Ve que el recurso no es nulo, omite el bloque sincronizado y devuelve una referencia a un Recurso parcialmente construido! Huelga decir que el resultado no es esperado ni deseado.

¿Puede ThreadLocal ayudar a reparar DCL?

Podemos usar ThreadLocal para lograr el objective explícito del modismo DCL: inicialización lenta sin sincronización en la ruta del código común. Considere esta versión (sin hilos) de DCL:

Listado 2. DCL usando ThreadLocal

 class ThreadLocalDCL { private static ThreadLocal initHolder = new ThreadLocal(); private static Resource resource = null; public Resource getResource() { if (initHolder.get() == null) { synchronized { if (resource == null) resource = new Resource(); initHolder.set(Boolean.TRUE); } } return resource; } } 

Creo; aquí, cada hilo entrará una vez en el bloque SYNC para actualizar el valor de threadLocal; entonces no lo hará. Entonces ThreadLocal DCL se asegurará de que un hilo ingrese solo una vez dentro del bloque SYNC.

¿Qué significa sincronizado realmente?

Java trata cada hilo como si se ejecutara en su propio procesador con su propia memoria local, cada uno hablando y sincronizándose con una memoria principal compartida. Incluso en un sistema de procesador único, ese modelo tiene sentido debido a los efectos de las memorias caché de memoria y el uso de registros de procesador para almacenar variables. Cuando un hilo modifica una ubicación en su memoria local, esa modificación debería aparecer también en la memoria principal, y el JMM define las reglas para cuando la JVM debe transferir datos entre la memoria local y la principal. Los arquitectos de Java se dieron cuenta de que un modelo de memoria demasiado restrictivo minaría seriamente el rendimiento del progtwig. Intentaron crear un modelo de memoria que permitiera a los progtwigs funcionar bien en el hardware de una computadora moderna y al mismo tiempo proporcionar garantías que permitieran a los hilos interactuar de manera predecible.

La herramienta principal de Java para representar interacciones entre hilos de forma predecible es la palabra clave sincronizada. Muchos progtwigdores piensan en sincronizar estrictamente en términos de hacer cumplir un semáforo de exclusión mutua (mutex) para evitar la ejecución de secciones críticas en más de un hilo a la vez. Desafortunadamente, esa intuición no describe completamente lo que significa sincronizar.

La semántica de sincronizados sí incluye la exclusión mutua de la ejecución en función del estado de un semáforo, pero también incluye reglas sobre la interacción del hilo de sincronización con la memoria principal. En particular, la adquisición o liberación de un locking desencadena una barrera de memoria: una sincronización forzada entre la memoria local y la memoria principal del hilo. (Algunos procesadores, como Alpha, tienen instrucciones explícitas de la máquina para realizar las barreras de memoria). Cuando un hilo sale de un bloque sincronizado, realiza una barrera de escritura: debe eliminar cualquier variable modificada en ese bloque en la memoria principal antes de liberarlo La cerradura. De manera similar, al ingresar a un bloque sincronizado, realiza una barrera de lectura: es como si la memoria local hubiera sido invalidada, y debe capturar cualquier variable a la que se haga referencia en el bloque desde la memoria principal.

Defina la variable que se debe verificar con el midificador volatile

No necesitas la variable h . Aquí hay un ejemplo de aquí

 class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) helper = new Helper(); } } return helper; } } 

¿A qué te refieres, de quién recibes la statement?

El locking con doble control es fijo. ver wikipedia:

 public class FinalWrapper { public final T value; public FinalWrapper(T value) { this.value = value; } } public class Foo { private FinalWrapper helperWrapper = null; public Helper getHelper() { FinalWrapper wrapper = helperWrapper; if (wrapper == null) { synchronized(this) { if (helperWrapper ==null) helperWrapper = new FinalWrapper( new Helper() ); wrapper = helperWrapper; } } return wrapper.value; } 

Como algunos han señalado, definitivamente necesita la palabra clave volatile para que funcione correctamente, a menos que todos los miembros en el objeto sean declarados final , de lo contrario no ocurrirá, antes de la publicación pr safe, y podría ver los valores predeterminados.

Nos cansamos de los constantes problemas con las personas que hacen esto mal, por lo que codificamos una utilidad LazyReference que tiene semántica final y ha sido perfilada y ajustada para que sea lo más rápida posible.

Copiando a continuación desde otro lugar, lo que explica por qué usar una variable local de método como una copia para la variable volátil acelerará las cosas.

Declaración que necesita explicación:

Este código puede parecer un poco intrincado. En particular, la necesidad del resultado de la variable local puede ser poco clara.

Explicación:

El campo se leerá por primera vez en la primera instrucción if y por segunda vez en la statement de devolución. El campo se declara volátil, lo que significa que debe volverse a almacenar desde la memoria cada vez que se accede (en términos generales, puede que se requiera aún más procesamiento para acceder a las variables volátiles) y el comstackdor no puede almacenarlo en un registro. Cuando se copia a la variable local y luego se usa en ambas declaraciones (si y retorno), la optimización de registros puede ser realizada por la JVM.

Si no me equivoco, también hay otra solución si no queremos usar la palabra clave volátil

por ejemplo tomando el ejemplo anterior

  class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) Helper newHelper = new Helper(); helper = newHelper; } } return helper; } } 

la prueba siempre está en la variable auxiliar pero la construcción del objeto se hace justo antes con el nuevoHelper, evita tener un objeto parcialmente construido