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.
- Estado inicial
field
M =null
field
A =null
field
B =null
- El hilo A realiza la primera comprobación nula, encuentra que el
field
A es nulo. - El hilo A adquiere el bloqueo de
this
. - El subproceso B realiza la primera comprobación de nulos, encuentra que el
field
B es nulo. - El subproceso B intenta obtener el bloqueo en
this
pero encuentra que está sujeto por el subproceso A. El subproceso B duerme - El hilo A realiza la segunda comprobación nula, encuentra que el
field
A es nulo. - El hilo A asigna al
field
A el valorfieldType1
y libera el bloqueo. Como elfield
no esvolatile
esta asignación no se propaga.
field
M =null
field
A =fieldType1
field
B =null
- El hilo B despierta y adquiere el bloqueo de
this
. - El subproceso B realiza la segunda comprobación de nulos, encuentra que el
field
B es nulo. - El hilo B asigna al
field
B el valorfieldType2
y libera el bloqueo.
field
M =null
field
A =fieldType1
field
B =fieldType2
- 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
- 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.