java multithreading concurrency order java-memory-model

java - Inmutabilidad y reordenamiento



multithreading concurrency (10)

UnsafeLazyInitialization.getInstance() nunca puede devolver null .

Voy a usar @ mesa de assylias.

Some Thread --------------------------------------------------------------------- 10: resource = null; //default value //write ===================================================================== Thread 1 | Thread 2 ----------------------------------+---------------------------------- 11: a = resource; | 21: x = resource; //read 12: if (a == null) | 22: if (x == null) 13: resource = new Resource(); | 23: resource = new Resource(); //write 14: b = resource; | 24: y = resource; //read 15: return b; | 25: return y;

Voy a usar los números de línea para el hilo de rosca 1 1. ve la escritura en 10 antes de la lectura, el 11, y la lectura en la línea 11 antes de la lectura de 14. Estos son intra-hilo sucede-antes las relaciones y no dicen nada de Tema 2. la lectura en la línea 14 devuelve un valor definido por el JMM. Dependiendo del momento, que puede ser el recurso creado en la línea 13, o puede ser cualquier valor escrito por Tema 2. Pero eso escritura tiene que suceder después de la lectura en la línea 11. Sólo hay un ejemplo de escritura, el inseguro publican en la línea 23. La escritura a nula en la línea 10 no está en el ámbito porque ocurrió antes de la línea 11 debido a intra pedido -thread.

No importa si Resourcees inmutable o no. La mayor parte de la discusión hasta ahora se ha centrado en la acción entre hilos en la inmutabilidad sería relevante, pero el reordenamiento que permitiría que este método devuelva nulo está prohibido por intra reglas -thread. La sección correspondiente de la especificación es JLS 17.4.7 .

Para cada hilo t, las acciones realizadas por T en una son los mismos que serían generadas por que el hilo en el programa de orden en el aislamiento, con cada escribir w escribiendo el valor V (w), dado que cada leer r ve el valor V (W (r)). Los valores vistos por cada lectura están determinadas por el modelo de memoria. El programa de orden dado debe reflejar el orden de programa en el que las acciones se realizan de acuerdo con la semántica intra-hilo de P.

Básicamente, esto significa que mientras lee y escribe puede ser reordenado, lee y escribe en la misma variable de tiene que aparecer como que suceden en el orden en el subproceso que ejecuta la lectura y escritura.

Sólo hay una sola escritura de null (en la línea 10). De cualquier hilo puede ver su propia copia del recurso o el otro hilo de, pero no puede ver la escritura anterior a nulo después se lee ya sea de recursos.

Como una nota, la inicialización a nulo tiene lugar en un hilo separado. La sección sobre la Publicación de seguridad en JCIP afirma:

Inicializadores estáticos son ejecutados por la JVM en tiempo de inicialización de clase; porque de sincronización interna en la JVM, este mecanismo está garantizado para publicar de forma segura los objetos inicializados de esta manera [JLS 12.4.2] .

Puede valer la pena tratar de escribir una prueba que pone UnsafeLazyInitialization.getInstance()a devolver null, y que obtiene parte de la propuesta equivalente reescribe a devolver null. Verá que no son realmente equivalentes.

EDITAR

He aquí un ejemplo que separa lee y escribe para mayor claridad. Digamos que hay un objeto variable estática pública.

public static Object object = new Integer(0);

1 hilo escribe a ese objeto:

object = new Integer(1); object = new Integer(2); object = new Integer(3);

2 hilo lee ese objeto:

System.out.println(object); System.out.println(object); System.out.println(object);

Sin ningún tipo de sincronización que proporciona entre hilos sucede antes relaciones, Tema 2 puede imprimir un montón de cosas diferentes.

1, 2, 3 0, 0, 0 3, 3, 3 1, 1, 3 etc.

Pero no puede imprimir una secuencia decreciente como 3, 2, 1. La semántica intra-hilo especificados en 17.4.7 limitar severamente la reordenación aquí. Si en lugar de utilizar objecttres veces cambiamos el ejemplo utilizar tres variables estáticas separadas, muchas más salidas serían posibles porque no habría ninguna restricción sobre reordenamiento.

Comenta sobre la respuesta aceptada

Esta pregunta ha generado mucho más calor del que yo hubiera imaginado. Una conclusión importante que obtuve de las discusiones públicas y privadas con los miembros de la lista de correo de interés concurrente (es decir, las personas que realmente trabajan en la implementación de esas cosas):

Si puede encontrar un reordenamiento secuencialmente consistente que no rompa ninguna relación entre pasadas sucede antes, es un reordenamiento válido (es decir, es compatible con la regla de orden del programa y el requisito de causalidad).

Eso es lo que John Vint ha proporcionado en su respuesta.

Pregunta original

El código siguiente (Java Concurrency in Practice listado 16.3) no es seguro para subprocesos por razones obvias:

public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { if (resource == null) resource = new Resource(); // unsafe publication return resource; } }

Sin embargo, unas pocas páginas más adelante, en la sección 16.3, dicen:

UnsafeLazyInitialization es realmente seguro si el Resource es inmutable.

No entiendo esa declaración:

  • si el Resource es inmutable, cualquier hilo que observe la variable de resource lo verá nulo o completamente construido (gracias a las fuertes garantías de los campos finales provistos por el modelo de memoria de Java)
  • sin embargo, nada impide el reordenamiento de la instrucción: en particular, las dos lecturas del resource podrían reordenarse (hay una lectura en el if y el otro en la return ). De modo que un hilo podría ver un resource no nulo en la condición if pero devolver una referencia nula (*).

Creo que UnsafeLazyInitialization.getInstance() puede devolver nulo incluso si el Resource es inmutable. ¿Es el caso y por qué (o por qué no)?

Nota: Espero una respuesta argumentada en lugar de declaraciones puras de sí o no.

(*) para comprender mejor mi punto sobre el reordenamiento, esta publicación del blog de Jeremy Manson, quien es uno de los autores del capítulo 17 del JLS sobre concurrencia, explica cómo el hashcode de String se publica de manera segura a través de una carrera de datos benigna y cómo eliminar el El uso de una variable local puede llevar a que el código hash retorne incorrectamente 0, debido a un posible reordenamiento muy similar al descrito anteriormente:

Lo que he hecho aquí es agregar una lectura adicional: la segunda lectura de hash, antes de la devolución. Por extraño que suene, y tan improbable como sea que suceda, la primera lectura puede devolver el valor hash calculado correctamente, y la segunda lectura puede devolver 0! Esto está permitido en el modelo de memoria porque el modelo permite un amplio reordenamiento de las operaciones. ¡La segunda lectura se puede mover, en su código, para que su procesador lo haga antes que el primero!


La confusión que creo que tiene aquí es lo que el autor quiso decir con una publicación segura. Él se estaba refiriendo a la publicación segura de un Recurso no nulo, pero pareces entenderlo.

Su pregunta es interesante: ¿es posible devolver un valor de recurso caché nulo?

Sí.

El compilador puede reordenar la operación como tal

public static Resource getInstance(){ Resource reordered = resource; if(resource != null){ return reordered; } return (resource = new Resource()); }

Esto no infringe la regla de coherencia secuencial, pero puede devolver un valor nulo.

Si esta es o no la mejor implementación está en debate, pero no hay reglas para evitar este tipo de reordenamiento.


Nada establece la referencia a null una vez que no es null . Es posible que un hilo vea null después de que otro hilo lo haya establecido como no null pero no veo cómo es posible hacerlo.

No estoy seguro de que la reordenación de la instrucción sea un factor aquí, pero el entrelazado de instrucciones por dos hilos sí lo está. La rama if no se puede reordenar de alguna manera para ejecutarse antes de que se haya evaluado su condición.


Lo siento si estoy equivocado (porque no soy nativo-Inglés de altavoces), pero parece a mí, que menciona la declaración:

UnsafeLazyInitialization es realmente seguro si los recursos es inmutable.

se debate fuera del contexto. Esta declaración es verdad con respecto a utilizar la seguridad de inicialización :

La garantía de la seguridad de inicialización permite construir adecuadamente los objetos inmutables ser compartidos de forma segura a través de hilos sin sincronización

...

inicialización de seguridad garantiza que los objetos construidos adecuadamente, todas las discusiones verán los valores correctos de los campos finales que fueron fijados por el constructor


De hecho, es seguro es UnsafeLazyInitialization.resourceque es inmutable, es decir, el campo está declarada como definitiva:

private static final Resource resource = new Resource();

También podría ser considerado como seguro para subprocesos si la Resourceclase en sí es inmutable y no importa qué instancia está utilizando. En ese caso, dos llamadas podrían volver distintas instancias de Resourcesin ningún problema, aparte de un aumento del consumo de memoria en función del número de hilos que llaman getInstance()al mismo tiempo).

Parece muy improbable -aunque y creo que hay un error tipográfico, frase real debería ser

UnsafeLazyInitialization es realmente segura si * r * esource es inmutable.


Después de leer a través del puesto se enlazó con más cuidado, estás en lo correcto, el ejemplo informados podría concebiblemente (bajo el modelo actual de la memoria) devuelve null. El ejemplo relevante es camino hacia abajo en los comentarios de la entrada, pero efectivamente, el tiempo de ejecución puede hacer esto:

public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { Resource tmp = resource; if (resource == null) tmp = resource = new Resource(); // unsafe publication return tmp; } }

Esto obedece a las limitaciones de un solo hilo, pero podría resultar en un valor de retorno nulo si múltiples hilos están llamando el método (la primera asignación a tmpconsigue un valor nulo, el si el bloque ve un valor no nulo, tmpobtiene devuelto como null) .

Con el fin de hacer de este "segura" inseguro (suponiendo recurso es inmutable), usted tiene que leer de forma explícita resourcesólo una vez (similar a cómo se debe tratar a una variable volátil compartida:

public class UnsafeLazyInitialization { private static Resource resource; public static Resource getInstance() { Resource cur = resource; if (cur == null) { cur = new Resource(); resource = cur; } return cur; } }


Después de aplicar las reglas de JLS a este ejemplo, he llegado a la conclusión de que getInstance definitivamente puede devolver null . En particular, JLS 17.4 :

El modelo de memoria determina qué valores se pueden leer en cada punto del programa. Las acciones de cada subproceso de forma aislada deben comportarse como se rigen por la semántica de ese subproceso, con la excepción de que los valores que ve cada lectura están determinados por el modelo de memoria .

Entonces está claro que en ausencia de sincronización, null es un resultado legal del método ya que cada una de las dos lecturas puede observar algo.

Prueba

Descomposición de lecturas y escrituras

El programa se puede descomponer de la siguiente manera (para ver claramente las lecturas y escrituras):

Some Thread --------------------------------------------------------------------- 10: resource = null; //default value //write ===================================================================== Thread 1 | Thread 2 ----------------------------------+---------------------------------- 11: a = resource; | 21: x = resource; //read 12: if (a == null) | 22: if (x == null) 13: resource = new Resource(); | 23: resource = new Resource(); //write 14: b = resource; | 24: y = resource; //read 15: return b; | 25: return y;

Lo que dice el JLS

JLS 17.4.5 da las reglas para que una lectura pueda observar una escritura:

Decimos que una lectura r de una variable v tiene permitido observar una escritura w a v si, en el orden anterior al recorrido parcial de la ejecución:

  • r no se ordena antes que w (es decir, no es el caso que hb (r, w)), y
  • no hay ninguna escritura entre w ''y v (es decir, no se escribe w'' a v tal que hb (w, w '') y hb (w'', r)).

Aplicación de la regla

En nuestro ejemplo, supongamos que el subproceso 1 ve nulo y correctamente inicializa el resource . En el hilo 2, una ejecución no válida sería para 21 para observar 23 (debido al orden del programa), pero cualquiera de las otras escrituras (10 y 13) puede ser observada por cualquiera de las siguientes:

  • 10 pasa, antes de todas las acciones, por lo que no se ordena lectura antes del 10
  • 21 y 24 no tienen ninguna relación hb con 13
  • 13 no sucede, antes de 23 (sin relación de hb entre los dos)

Entonces, tanto 21 como 24 (nuestras 2 lecturas) pueden observar 10 (nulo) o 13 (no nulo).

Ruta de ejecución que devuelve nulo

En particular, suponiendo que el subproceso 1 ve un nulo en la línea 11 e inicializa el resource en la línea 13, el subproceso 2 podría ejecutarse legalmente de la siguiente manera:

  • 24: y = null (lee escritura 10)
  • 21: x = non null (lee, escribe 13)
  • 22: false
  • 25: return y

Nota: para aclarar, esto no significa que T2 ve no nulo y posteriormente ve nulo (lo que violaría los requisitos de causalidad) - significa que desde una perspectiva de ejecución, las dos lecturas se han reordenado y la segunda se confirmó antes de la primera uno - sin embargo, parece que la última escritura se había visto antes que la anterior basada en el orden del programa inicial.

ACTUALIZACIÓN 10 de febrero

De regreso al código, un reordenamiento válido sería:

Resource tmp = resource; // null here if (resource != null) { // resource not null here resource = tmp = new Resource(); } return tmp; // returns null

Y dado que el código es consecuentemente consecuente (si se ejecuta con un solo hilo, siempre tendrá el mismo comportamiento que el código original), muestra que se cumplen los requisitos de causalidad (hay una ejecución válida que produce el resultado).

Después de publicar en la lista de interés de concurrencia, recibí algunos mensajes sobre la legalidad de ese reordenamiento, que confirma que null es un resultado legal:

  • La transformación es definitivamente legal ya que una ejecución de un solo subproceso no notará la diferencia. [Tenga en cuenta que] la transformación no parece sensata; no hay una buena razón para que un compilador lo haga. Sin embargo, dada una mayor cantidad de código circundante o quizás un "error" de optimización del compilador, podría suceder.
  • La afirmación sobre el orden dentro del hilo y el orden del programa es lo que me hizo cuestionar la validez de las cosas, pero finalmente el JMM se relaciona con el bytecode que se ejecuta. La transformación podría ser realizada por el compilador javac, en cuyo caso nulo será perfectamente válido. Y no hay reglas sobre cómo javac tiene que convertir de código Java byte a Java así que ...

Hay esencialmente dos preguntas que estás preguntando:

1. ¿Puede el método getInstance() devolver null debido al reordenamiento?

(que creo que es lo que realmente buscas, así que intentaré responderlo primero)

Aunque creo que diseñar Java para permitir esto es una locura, parece que de hecho es correcto que getInstance() puede devolver nulo.

Su código de ejemplo:

if (resource == null) resource = new Resource(); // unsafe publication return resource;

es lógicamente 100% idéntico al ejemplo en la publicación de blog a la que vinculó:

if (hash == 0) { // calculate local variable h to be non-zero hash = h; } return hash;

Jeremy Manson luego describe que su código puede devolver 0 debido al reordenamiento. Al principio, no lo creía, ya que pensé que el siguiente "sucede antes" -logic debe contener:

"if (resource == null)" happens before "resource = new Resource();" and "resource = new Resource();" happens before "return resource;" therefore "if (resource == null)" happens before "return resource;", preventing null

Pero Jeremy da el siguiente ejemplo en un comentario a su publicación de blog, sobre cómo este código podría ser reescrito válidamente por el compilador:

read = resource; if (resource==null) read = resource = new Resource(); return read;

Esto, en un entorno de subproceso único, se comporta de forma idéntica al código original, pero en un entorno de subprocesos múltiples puede generar el siguiente orden de ejecución:

Thread 1 Thread 2 ------------------------------- ------------------------------------------------- read = resource; // null read = resource; // null if (resource==null) // true read = resource = new Resource(); // non-null return read; // non-null if (resource==null) // FALSE!!! return read; // NULL!!!

Ahora, desde un punto de vista de optimización, hacer esto no tiene ningún sentido para mí, ya que el objetivo de estas cosas sería reducir lecturas múltiples en la misma ubicación, en cuyo caso no tiene sentido que el compilador no lo haga. generar if (read==null) lugar, evitando el problema. Entonces, como señala Jeremy en su blog, es muy poco probable que esto suceda. Pero parece que, puramente desde el punto de vista de las reglas del lenguaje, de hecho está permitido.

Este ejemplo está cubierto en realidad en JLS:

http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.4

El efecto observado entre los valores de r2 , r4 y r5 en la Table 17.4. Surprising results caused by forward substitution Table 17.4. Surprising results caused by forward substitution son equivalentes a lo que puede suceder con read = resource , if (resource==null) y el return resource en el ejemplo anterior.

Aparte: ¿Por qué hago referencia a la publicación del blog como la fuente principal de la respuesta? ¡Porque el tipo que lo escribió, también es el tipo que escribió el capítulo 17 de JLS sobre la concurrencia! Entonces, ¡será mejor que esté en lo cierto! :)

2. ¿Hacer que el Resource inmutable hace que el método getInstance() seguro para subprocesos?

Dado el posible resultado null , que puede ocurrir independientemente de si el Resource es mutable o no, la respuesta simple e inmediata a esta pregunta es: No (no estrictamente)

Si ignoramos este escenario altamente improbable pero posible, la respuesta es: Depende .

El obvio problema de enhebrado con el código es que puede llevar al siguiente orden de ejecución (sin necesidad de ningún reordenamiento):

Thread 1 Thread 2 ---------------------------------------- ---------------------------------------- if (resource==null) // true; if (resource==null) // true resource=new Resource(); // object 1 return resource; // object 1 resource=new Resource(); // object 2 return resource; // object 2

Por lo tanto, la seguridad sin hilos proviene del hecho de que puede recuperar dos objetos diferentes de la función (aunque sin reordenar ninguno de ellos será null ).

Ahora, lo que el libro probablemente intentaba decir es lo siguiente:

Los objetos inmutables de Java, como Strings e Integers, intentan evitar la creación de múltiples objetos para el mismo contenido. Entonces, si tiene "hello" en un lugar y "hello" en otro lugar, Java le dará la misma referencia de objeto exacta. De forma similar, si tiene un new Integer(5) en un punto y un new Integer(5) en otro. Si este fuera el caso con el new Resource() también, obtendría la misma referencia y el object 1 y el object 2 en el ejemplo anterior sería exactamente el mismo objeto. De hecho, esto llevaría a una función efectiva a prueba de hilos (ignorando el problema de reordenamiento).

Pero, si implementa el Resource usted mismo, no creo que haya siquiera una manera de hacer que el constructor devuelva una referencia a un objeto previamente creado en lugar de crear uno nuevo. Por lo tanto, no debería poder hacer que el object 1 y el object 2 sean exactamente el mismo objeto. Pero, dado que llama al constructor con los mismos argumentos (ninguno en ambos casos), es probable que, aunque sus objetos creados no sean el mismo objeto exacto, se comporten, a todos los efectos, como si lo fueran, también haciendo que el código sea seguro para las cadenas.

Sin embargo, esto no necesariamente tiene que ser el caso. Imagine una versión inmutable de Date , por ejemplo. El constructor predeterminado Date() usa la hora actual del sistema como el valor de la fecha. Entonces, aunque el objeto es inmutable y se llama al constructor con el mismo argumento, llamarlo dos veces probablemente no resultará en un objeto equivalente. Por lo tanto, el método getInstance() no es seguro para subprocesos.

Entonces, como afirmación general, creo que la línea que citó del libro es simplemente errónea (al menos, como fuera de contexto aquí).

ADICIÓN Re: reordenamiento

Encuentro que el resource==new Resource() ejemplo es demasiado simplista para ayudarme a entender POR QUÉ permitir que tal reordenamiento por Java tenga algún sentido. Así que déjame ver si puedo encontrar algo que ayude a la optimización:

System.out.println("Found contact:"); System.out.println(firstname + " " + lastname); if (firstname==null) firstname = ""; if (lastname ==null) lastname = ""; return firstname + " " + lastname;

Aquí, en el caso más probable de que ambos ifs false , no es óptimo hacer el costoso concatenación de cadena nombre firstname + " " + lastname dos veces, una vez para el mensaje de depuración, una vez para el retorno. Por lo tanto, tendría sentido aquí reordenar el código para hacer lo siguiente en su lugar:

System.out.println("Found contact:"); String contact = firstname + " " + lastname; System.out.println(contact); if ((firstname==null) || (lastname==null)) { if (firstname==null) firstname = ""; if (lastname ==null) lastname = ""; contact = firstname + " " + lastname; } return contact;

A medida que los ejemplos se vuelven más complejos y cuando empiezas a pensar que el compilador realiza un seguimiento de lo que ya está cargado / computado en los registros del procesador que usa y omite inteligentemente el recálculo de los resultados ya existentes, este efecto podría ser cada vez más probable ocurrir. Entonces, aunque nunca pensé que podría decir esto cuando me acosté anoche, pensando más en ello, en realidad ahora creo que esta puede haber sido una decisión necesaria / buena para permitir realmente la optimización de código para hacer su mayor esfuerzo. impresionante magia. Pero todavía me parece bastante peligroso, ya que no creo que mucha gente lo sepa e incluso si lo son, es bastante complejo entender cómo escribir el código correctamente sin sincronizar todo (lo que luego desaparecerá). muchas veces con los beneficios de rendimiento obtenidos de una optimización más flexible).

Supongo que si no permitieras este reordenamiento, cualquier almacenamiento en caché y reutilización de resultados intermedios de una serie de pasos de proceso se volvería ilegal, eliminando así una de las optimizaciones de compilador más potentes posibles.


ACTUALIZACIÓN Feb10

Me estoy convenciendo de que deberíamos separar 2 fases: compilación y ejecución .

Creo que el factor de decisión si está permitido devolver nulo o no es lo que es el bytecode . Hice 3 ejemplos:

Ejemplo 1:

El código fuente original, traducido literalmente a bytecode:

if (resource == null) resource = new Resource(); // unsafe publication return resource;

El bytecode:

public static Resource getInstance(); Code: 0: getstatic #20; //Field resource:LResource; 3: ifnonnull 16 6: new #22; //class Resource 9: dup 10: invokespecial #24; //Method Resource."<init>":()V 13: putstatic #20; //Field resource:LResource; 16: getstatic #20; //Field resource:LResource; 19: areturn

Este es el caso más interesante, porque hay 2 read (Línea # 0 y Línea # 16), y hay 1 write entre líneas (Línea # 13). Reclamo que no es posible reordenar , pero examinemos a continuación.

Ejemplo 2 :

El código "complier optimized", que puede ser literalmente reconvertido a java de la siguiente manera:

Resource read = resource; if (resource==null) read = resource = new Resource(); return read;

El código de bytes para eso (en realidad lo produje compilando el fragmento de código anterior):

public static Resource getInstance(); Code: 0: getstatic #20; //Field resource:LResource; 3: astore_0 4: getstatic #20; //Field resource:LResource; 7: ifnonnull 22 10: new #22; //class Resource 13: dup 14: invokespecial #24; //Method Resource."<init>":()V 17: dup 18: putstatic #20; //Field resource:LResource; 21: astore_0 22: aload_0 23: areturn

Es obvio que si el compilador "optimiza" y se produce el código de bytes como se muestra arriba, puede producirse una lectura nula (por ejemplo, me refiero al blog de Jeremy Manson )

También es interesante ver cómo a = b = c funciona: la referencia a la nueva instancia (Línea # 14) está duplicada (Línea # 17), y la misma referencia se almacena entonces, primero a b (recurso, (Línea) # 18)) luego a a (leer, (Línea # 21)).

Ejemplo 3 :

Hagamos una modificación aún más ligera: ¡lea el resource solo una vez! Si el compilador comienza a optimizar (y usa registros, como otros mencionaron), esta es una mejor optimización que la anterior , porque la Línea 4 es un "acceso de registro" en lugar de un "acceso estático" más caro en el Ejemplo 2.

Resource read = resource; if (read == null) // reading the local variable, not the static field read = resource = new Resource(); return read;

El bytecode para el Ejemplo 3 (también creado con la compilación literal de arriba):

public static Resource getInstance(); Code: 0: getstatic #20; //Field resource:LResource; 3: astore_0 4: aload_0 5: ifnonnull 20 8: new #22; //class Resource 11: dup 12: invokespecial #24; //Method Resource."<init>":()V 15: dup 16: putstatic #20; //Field resource:LResource; 19: astore_0 20: aload_0 21: areturn

También es fácil ver que no es posible obtener un valor nulo de este bytecode ya que se construye de la misma manera que String.hashcode() , teniendo solo 1 lectura de la variable estática del resource .

Ahora veamos el Ejemplo 1 :

0: getstatic #20; //Field resource:LResource; 3: ifnonnull 16 6: new #22; //class Resource 9: dup 10: invokespecial #24; //Method Resource."<init>":()V 13: putstatic #20; //Field resource:LResource; 16: getstatic #20; //Field resource:LResource; 19: areturn

Puede ver que la Línea # 16 (la lectura de la variable#20 para el retorno) más observa la escritura de la Línea # 13 (la asignación de la variable#20 del constructor), por lo que es ilegal colocarla delante en cualquier orden de ejecución donde La línea # 13 se ejecuta . Por lo tanto, no es posible reordenar .

Para una JVM, es posible construir (y aprovechar) una rama que (usando ciertas condiciones adicionales) pasa por alto la línea # 13 escribir: la condición es que la lectura de la variable#20 no debe ser nula .

Entonces, en ningún caso para el Ejemplo 1 es posible devolver nulo.

Conclusión:

Al ver los ejemplos anteriores, un bytecode visto en el Ejemplo 1 NO PRODUCIRá null . Un byte optimizado como en el ejemplo 2 SERÁ PROCESO null , pero hay una optimización aún mejor del ejemplo 3 , que NO PRODUCIRá null .

Como no podemos estar preparados para todas las posibles optimizaciones de todos los compiladores, podemos decir que en algunos casos es posible, en otros casos no es posible return null , y todo depende del código de bytes. Además, hemos demostrado que hay al menos un ejemplo para ambos casos .

Un razonamiento más antiguo : refiriéndose al ejemplo de Assylias: La pregunta principal es: ¿es válido (con respecto a todas las especificaciones, JMM, JLS) que una máquina virtual reordene las lecturas 11 y 14, entonces 14 ocurrirán ANTES de las 11?

Si pudiera suceder, entonces el Thread2 independiente podría escribir el recurso con 23, por lo que 14 podría leer null . Declaro que no es posible .

En realidad, debido a que existe una posible escritura de 13, no sería un orden de ejecución válido . Una VM puede optimizar el orden de ejecución, excluyendo las ramas no ejecutadas (quedando solo 2 lecturas, no escrituras), pero para tomar esta decisión, debe hacer la primera lectura (11), y debe leer no nulo , entonces la lectura 14 no puede preceder a la lectura 11 . Por lo tanto, NO es posible devolver null .

Inmutabilidad

Con respecto a la inmutabilidad, creo que esta afirmación no es verdadera:

UnsafeLazyInitialization es realmente seguro si el Recurso es inmutable.

Sin embargo, si el constructor es impredecible, pueden surgir resultados interesantes. Imagine un constructor como este:

public class Resource { public final double foo; public Resource() { this.foo = Math.random(); } }

Si tenemos Thread , puede resultar que los 2 hilos reciban un Objeto de comportamiento diferente. Entonces, la declaración completa debería sonar así:

UnsafeLazyInitialization es realmente seguro si el Recurso es inmutable y su inicialización es consistente.

Por consistente quiero decir que al llamar al constructor del Resource dos veces recibiremos dos objetos que se comportan exactamente de la misma manera (llamar a los mismos métodos en el mismo orden en ambos arrojará los mismos resultados).


Esto es ahora un hilo muy atrás, aún dado que esta pregunta discute muchos funcionamientos interesantes de reordenamiento y concurrencia, estoy involucrado aquí por aunque últimamente.

Por un momento, si no involucramos concurrencia, las acciones y los reordenamientos válidos en una situación de subprocesos múltiples.
"¿Puede JVM utilizar una operación de escritura de valor en caché en contexto de subproceso único?". Creo que no. Dado que hay una operación de escritura en la condición si el almacenamiento en caché puede entrar para jugar.
Volviendo a la pregunta, la inmutabilidad asegura que el objeto se cree completa o correctamente antes de que su referencia sea accesible o publicada, por lo que la inmutabilidad definitivamente ayuda. Pero aquí hay una operación de escritura después de la creación del objeto. Entonces, ¿puede el segundo leer en caché el valor de preescritura, en el mismo hilo u otro? No. Un hilo puede no saber sobre la escritura en otro hilo (dado que no hay necesidad de visibilidad inmediata entre los hilos). Entonces, la posibilidad de devolver un nulo falso (es decir, después de la creación del objeto) no será válida. (El código en cuestión rompe singleton, pero no estamos molestos por el aquí)