Entonces, después de algunas entrevistas de trabajo, quería escribir un pequeño programa para verificar que i++ realmente no sea atómico en Java, y que uno debería, en la práctica, agregar algún bloqueo para protegerlo. Resulta que deberías, pero esta no es la pregunta aquí.
Así que escribí este programa aquí solo para comprobarlo.
El caso es que se cuelga. Parece que el subproceso principal está atascado en la línea t1.join() , aunque ambos subprocesos de trabajo deberían finalizar debido a stop = true de la línea anterior.
Descubrí que el ahorcamiento se detiene si:
boolean stop como volatile , lo que hace que los subprocesos de trabajo vean inmediatamente la escritura, ot como volatile ... para esto no tengo idea de qué causa el descuelgue.¿Alguien puede explicar lo que está pasando? ¿Por qué veo el cuelgue y por qué se detiene en esos tres casos?
public class Test { static /* volatile */ long t = 0; static long[] counters = new long[2]; static /* volatile */ boolean stop = false; static Object o = new Object(); public static void main(String[] args) { Thread t1 = createThread(0); Thread t2 = createThread(1); t1.start(); t2.start(); Thread.sleep(1000); stop = true; t1.join(); t2.join(); System.out.println("counter : " + t + " counters : " + counters[0] + ", " + counters[1] + " (sum : " + (counters[0] + counters[1]) + ")"); } private static Thread createThread(final int i) { Thread thread = new Thread() { public void run() { while (!stop) { // synchronized (o) { t++; // } // if (counters[i] % 1000000 == 0) // { // System.out.println(i + ")" + counters[i]); // } counters[i]++; } }; }; return thread; } }Parece que el subproceso principal está atascado en la línea
t1.join(), aunque ambos subprocesos de trabajo deberían finalizar debido astop = truede la línea anterior.
En ausencia de volatile , bloqueo u otro mecanismo de publicación seguro, la JVM no tiene la obligación de hacer que stop = true sea visible para otros subprocesos. Aplicado específicamente a su caso, mientras su subproceso principal duerme durante un segundo, el compilador JIT optimiza su ciclo activo while (!stop) en el equivalente de
if (!stop) { while (true) { ... } }Esta optimización particular se conoce como "elevación" de la acción de lectura fuera del ciclo.
Descubrí que el ahorcamiento se detiene si:
- Agrego algo de impresión dentro de los subprocesos de trabajo (como en los comentarios), lo que probablemente provoque que los subprocesos de trabajo abandonen la CPU en algún momento
No, es porque PrintStream::println es un método sincronizado. Todas las JVM conocidas emitirán una valla de memoria en el nivel de la CPU para garantizar la semántica de una acción de "adquirir" (en este caso, bloquear la adquisición), y esto forzará una recarga de la variable de stop . Esto no es requerido por la especificación, solo una opción de implementación.
- Si marco la
boolean stopde bandera como volátil, lo que hace que los subprocesos de trabajo vean inmediatamente la escritura
La especificación en realidad no tiene requisitos de tiempo de reloj de pared sobre cuándo una escritura volátil debe volverse visible para otros subprocesos, pero en la práctica se entiende que debe volverse visible "muy pronto". Por lo tanto, este cambio es la forma correcta de garantizar que la escritura para stop se publique de manera segura y, posteriormente, sea observada por otros subprocesos que la lean.
- Si marco el contador
tcomo volátil... para esto no tengo idea de qué causa el descuelgue.
Estos son nuevamente los efectos indirectos de lo que hace la JVM para garantizar la semántica de una lectura volatile , que es otro tipo de acción entre subprocesos de "adquisición".
En resumen, a excepción del cambio que hace que stop sea una variable volátil, su programa pasa de colgarse para siempre a completarse debido a los efectos secundarios accidentales de la implementación de JVM subyacente, que por simplicidad hace más vaciado/invalidación del estado local del subproceso de lo requerido por la especificación.
Esas pueden ser las posibles razones:
Si está interesado en profundizar en el tema, le sugiero que consulte el libro "Java Concurrency in Practice de Brian Goetz".
Marcar una variable como volátil es una sugerencia para la JVM, para vaciar/sincronizar los segmentos relacionados de caché entre subprocesos/núcleos cuando se actualiza esa variable. Marcar stop como volátil tiene un mejor comportamiento (pero no perfecto, es posible que tenga algunas ejecuciones adicionales en sus subprocesos antes de que vean la actualización).
Marcar t como volátil me desconcierta por qué funciona, puede ser que debido a que este es un programa tan pequeño, t y stop están en la misma fila en el caché, por lo que cuando uno se descarga/sincroniza, el otro también lo hace.
System.out.println es seguro para subprocesos, por lo que hay cierta sincronización interna. Nuevamente, esto puede estar causando que algunas partes del caché se sincronicen entre los subprocesos.
Si alguien puede agregar a esto, por favor hágalo, también me gustaría escuchar una respuesta más detallada sobre esto.