studio programacion para móviles libro edición desarrollo desarrollar curso aprende aplicaciones java multithreading thread-safety final jls

java - para - manual de programacion android pdf



Pruebas de inicialización de seguridad de campos finales. (8)

Estoy intentando simplemente probar la seguridad de inicialización de los campos finales garantizada por el JLS. Es para un papel que estoy escribiendo. Sin embargo, no puedo hacer que falle en función de mi código actual. ¿Alguien puede decirme qué estoy haciendo mal, o si esto es algo que tengo que repetir una y otra vez y luego ver un error con un poco de mala suerte?

Aquí está mi código:

public class TestClass { final int x; int y; static TestClass f; public TestClass() { x = 3; y = 4; } static void writer() { TestClass.f = new TestClass(); } static void reader() { if (TestClass.f != null) { int i = TestClass.f.x; // guaranteed to see 3 int j = TestClass.f.y; // could see 0 System.out.println("i = " + i); System.out.println("j = " + j); } } }

y mis hilos lo llaman así:

public class TestClient { public static void main(String[] args) { for (int i = 0; i < 10000; i++) { Thread writer = new Thread(new Runnable() { @Override public void run() { TestClass.writer(); } }); writer.start(); } for (int i = 0; i < 10000; i++) { Thread reader = new Thread(new Runnable() { @Override public void run() { TestClass.reader(); } }); reader.start(); } } }

He ejecutado este escenario muchas, muchas veces. Mis bucles actuales están generando 10,000 hilos, pero he terminado con este 1000, 100000 e incluso un millón. Todavía no hay fracaso. Siempre veo 3 y 4 para ambos valores. ¿Cómo puedo conseguir que esto falle?


Me gustaría ver una prueba que falla o una explicación de por qué no es posible con las JVM actuales.

Multihilo y Pruebas

No puede probar que una aplicación de multiproceso está dañada (o no) probando por varias razones:

  • el problema solo puede aparecer una vez cada x horas de ejecución, siendo x tan alto que es poco probable que lo veas en una prueba corta
  • el problema solo puede aparecer con algunas combinaciones de arquitecturas JVM / procesador

En su caso, para hacer la pausa de prueba (es decir, observar y == 0), el programa debería ver un objeto construido parcialmente donde algunos campos se han construido correctamente y otros no. Esto normalmente no sucede en x86 / hotspot.

¿Cómo determinar si un código multiproceso está roto?

La única manera de probar que el código es válido o está roto es aplicarle las reglas JLS y ver cuál es el resultado. Con la publicación de la carrera de datos (sin sincronización alrededor de la publicación del objeto o de y), el JLS no ofrece ninguna garantía de que y se verá como 4 (se podría ver con su valor predeterminado de 0).

¿Puede ese código realmente romper?

En la práctica, algunas JVM serán mejores para hacer que la prueba falle. Por ejemplo, algunos compiladores (ver "Un caso de prueba que muestra que no funciona" en este artículo ) podrían transformar TestClass.f = new TestClass(); en algo como (porque se publica a través de una carrera de datos):

(1) allocate memory (2) write fields default values (x = 0; y = 0) //always first (3) write final fields final values (x = 3) //must happen before publication (4) publish object //TestClass.f = new TestClass(); (5) write non final fields (y = 4) //has been reodered after (4)

El JLS exige que (2) y (3) ocurran antes de la publicación del objeto (4). Sin embargo, debido a la carrera de datos, no se otorga ninguna garantía para (5): en realidad sería una ejecución legal si un subproceso nunca observara esa operación de escritura. Con el intercalado de hilos adecuado, es por lo tanto concebible que si el reader ejecuta entre 4 y 5, obtendrá el resultado deseado.

No tengo un JIT de Symantec a mano, así que no puedo probarlo experimentalmente :-)


¿Qué está pasando en este hilo? ¿Por qué debería fallar ese código en primer lugar?

Lanzas miles de hilos que harán lo siguiente:

TestClass.f = new TestClass();

Lo que eso hace, en orden:

  1. Evaluar TestClass.f para averiguar su ubicación de memoria
  2. evaluar new TestClass() : esto crea una nueva instancia de TestClass, cuyo constructor inicializará tanto x como y
  3. asignar el valor de la mano derecha a la ubicación de la memoria de la mano izquierda

Una asignación es una operación atómica que siempre se realiza después de que se haya generado el valor de la mano derecha . Aquí hay una cita de la especificación del lenguaje Java (vea el primer punto con viñetas) pero realmente se aplica a cualquier lenguaje sano.

Esto significa que mientras el constructor TestClass() está tomando su tiempo para hacer su trabajo, y es posible que x e y aún sean cero, la referencia al objeto TestClass parcialmente inicializado solo vive en la pila de ese hilo o en los registros de la CPU, y no sido escrito en TestClass.f

Por TestClass.f tanto, TestClass.f siempre contendrá:

  • o bien null , al inicio de su programa, antes de que se le asigne algo más,
  • o una instancia TestClass completamente inicializada.

¿Qué hay de usted modificó el constructor para hacer esto?

public TestClass() { Thread.sleep(300); x = 3; y = 4; }

No soy un experto en finales de JLF e inicializadores, pero el sentido común me dice que esto debería retrasar la configuración x lo suficiente para que los escritores registren otro valor.


¿Qué pasa si uno cambia el escenario en

public class TestClass { final int x; static TestClass f; public TestClass() { x = 3; } int y = 4; // etc... }

?


Desde Java 5.0, está garantizado que todos los subprocesos verán el estado final establecido por el constructor.

Si desea ver que esto falla, puede probar una JVM más antigua como 1.3.

No imprimiría todas las pruebas, solo imprimiría las fallas. Usted podría obtener un fracaso en un millón, pero te lo pierdas. Pero si solo imprime fallas, deberían ser fáciles de detectar.

Una forma más sencilla de ver este error es agregar al escritor.

f.y = 5;

y prueba para

int y = TestClass.f.y; // could see 0, 4 or 5 if (y != 5) System.out.println("y = " + y);


Una mejor comprensión de por qué esta prueba no falla puede venir de la comprensión de lo que realmente sucede cuando se invoca al constructor. Java es un lenguaje basado en la pila. TestClass.f = new TestClass(); consta de cuatro acciones. Se llama la primera new instrucción, es como malloc en C / C ++, asigna memoria y coloca una referencia a ella en la parte superior de la pila. Luego se duplica la referencia para invocar un constructor. De hecho, Constructor es como cualquier otro método de instancia, se invoca con la referencia duplicada. Solo después de que esa referencia se almacene en el marco del método o en el campo de la instancia y sea accesible desde cualquier otro lugar. Antes de que el último paso, la referencia al objeto esté presente solo en la parte superior de la creación de la pila de hilos y nadie más pueda verlo. De hecho, no hay ninguna diferencia con el tipo de campo en el que está trabajando, ambos se inicializarán si TestClass.f != null . Puede leer los campos x e y de diferentes objetos, pero esto no dará como resultado y = 0 . Para obtener más información, debe ver los artículos sobre lenguaje de programación orientado a la pila y la especificación de JVM .

UPD : Una cosa importante que olvidé mencionar. Por la memoria java no hay forma de ver el objeto parcialmente inicializado. Si no haces auto publicaciones dentro de constructor, seguro.

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

Se considera que un objeto está completamente inicializado cuando finaliza su constructor. Se garantiza que un subproceso que solo puede ver una referencia a un objeto después de que ese objeto se haya inicializado completamente ve los valores correctamente inicializados para los campos finales de ese objeto.

JLS :

Hay un borde antes del final de un constructor de un objeto hasta el inicio de un finalizador para ese objeto.

Explicación más amplia de este punto de vista :

Resulta que el final del constructor de un objeto ocurre antes de la ejecución de su método de finalización. En la práctica, lo que esto significa es que cualquier escritura que ocurra en el constructor debe estar terminada y visible para cualquier lectura de la misma variable en el finalizador, como si esas variables fueran volátiles.

UPD : Esa era la teoría, pasemos a la práctica.

Considere el siguiente código, con variables simples no finales:

public class Test { int myVariable1; int myVariable2; Test() { myVariable1 = 32; myVariable2 = 64; } public static void main(String args[]) throws Exception { Test t = new Test(); System.out.println(t.myVariable1 + t.myVariable2); } }

El siguiente comando muestra las instrucciones de la máquina generadas por java, cómo usarlas en un wiki :

java.exe -XX: + UnlockDiagnosticVMOptions -XX: + PrintAssembly -Xcomp -XX: PrintAssemblyOptions = hsdis-print-bytes -XX: CompileCommand = print, * Test.main Test

Es salida:

... 0x0263885d: movl $0x20,0x8(%eax) ;...c7400820 000000 ;*putfield myVariable1 ; - Test::<init>@7 (line 12) ; - Test::main@4 (line 17) 0x02638864: movl $0x40,0xc(%eax) ;...c7400c40 000000 ;*putfield myVariable2 ; - Test::<init>@13 (line 13) ; - Test::main@4 (line 17) 0x0263886b: nopl 0x0(%eax,%eax,1) ;...0f1f4400 00 ...

Las asignaciones de campo son seguidas por la instrucción NOPL , uno de sus propósitos es evitar el reordenamiento de la instrucción .

¿Por qué pasó esto? De acuerdo con la especificación, la finalización ocurre después de que el constructor regrese. Así que el hilo GC no puede ver un objeto parcialmente inicializado. En un nivel de CPU, el subproceso GC no se distingue de ningún otro subproceso. Si tales garantías se proporcionan a GC, entonces se proporcionan a cualquier otro hilo. Esta es la solución más obvia a tal restricción.

Resultados:

1) El constructor no está sincronizado, la sincronización se realiza mediante otras instrucciones .

2) La asignación a la referencia del objeto no puede ocurrir antes de que el constructor regrese.


Yo escribí la especificación. El TL; La versión DR de esta respuesta es que solo porque puede ver 0 para y, eso no significa que se garantice que vea 0 para y.

En este caso, la especificación de campo final garantiza que verá 3 para x, como señala. Piensa que el hilo del escritor tiene 4 instrucciones:

r1 = <create a new TestClass instance> r1.x = 3; r1.y = 4; f = r1;

La razón por la que podría no ver 3 para x es si el compilador reordenó este código:

r1 = <create a new TestClass instance> f = r1; r1.x = 3; r1.y = 4;

La forma en que generalmente se implementa la garantía de los campos finales en la práctica es asegurar que el constructor finalice antes de que se realicen las acciones subsiguientes del programa. Imagina que alguien construyó una gran barrera entre r1.y = 4 y f = r1. Por lo tanto, en la práctica, si tiene algún campo final para un objeto, es probable que obtenga visibilidad para todos ellos.

Ahora, en teoría, alguien podría escribir un compilador que no esté implementado de esa manera. De hecho, muchas personas a menudo han hablado de probar el código escribiendo el compilador más malicioso posible. Esto es particularmente común entre las personas de C ++, que tienen muchos rincones indefinidos de su lenguaje que pueden llevar a errores terribles.


Here es un ejemplo de los valores predeterminados de los valores no finales que se observan a pesar de que el constructor los establece y no filtra this . Esto se basa en mi otra pregunta que es un poco más complicada. Sigo viendo que la gente dice que no puede suceder en x86, pero mi ejemplo sucede en x64 linux openjdk 6 ...