java concurrency deadlock jvm-hotspot static-initialization

java - ¿Por qué el uso de flujos paralelos en el inicializador estático conduce a un interbloqueo no estable



concurrency deadlock (1)

PRECAUCIÓN: no es un duplicado, lea el tema cuidadosamente https://stackoverflow.com/users/3448419/apangin quote:

La verdadera pregunta es por qué el código a veces funciona cuando no debería. El tema se reproduce incluso sin lambdas. Esto me hace pensar que podría haber un error de JVM.

En los comentarios de https://stackoverflow.com/a/53709217/2674303 traté de averiguar las razones por las que el código se comporta de manera diferente de un comienzo a otro y los participantes de esa discusión me hicieron un consejo para crear un tema separado.

No consideremos el siguiente código fuente:

public class Test { static { System.out.println("static initializer: " + Thread.currentThread().getName()); final long SUM = IntStream.range(0, 5) .parallel() .mapToObj(i -> { System.out.println("map: " + Thread.currentThread().getName() + " " + i); return i; }) .sum(); } public static void main(String[] args) { System.out.println("Finished"); } }

A veces (casi siempre) lleva a un punto muerto.

Ejemplo de salida:

static initializer: main map: main 2 map: ForkJoinPool.commonPool-worker-3 4 map: ForkJoinPool.commonPool-worker-3 3 map: ForkJoinPool.commonPool-worker-2 0

Pero a veces termina con éxito (muy raro):

static initializer: main map: main 2 map: main 3 map: ForkJoinPool.commonPool-worker-2 4 map: ForkJoinPool.commonPool-worker-1 1 map: ForkJoinPool.commonPool-worker-3 0 Finished

o

static initializer: main map: main 2 map: ForkJoinPool.commonPool-worker-2 0 map: ForkJoinPool.commonPool-worker-1 1 map: ForkJoinPool.commonPool-worker-3 4 map: main 3

¿Podrías explicar ese comportamiento?


TL; DR Este es un error HotSpot JDK-8215634

El problema se puede reproducir con un caso de prueba simple que no tiene ninguna raza:

public class StaticInit { static void staticTarget() { System.out.println("Called from " + Thread.currentThread().getName()); } static { Runnable r = new Runnable() { public void run() { staticTarget(); } }; r.run(); Thread thread2 = new Thread(r, "Thread-2"); thread2.start(); try { thread2.join(); } catch (Exception ignore) {} System.out.println("Initialization complete"); } public static void main(String[] args) { } }

Esto parece un interbloqueo de inicialización clásico, pero HotSpot JVM no se bloquea. En su lugar se imprime:

Called from main Called from Thread-2 Initialization complete

¿Por qué esto es un error?

JVMS §6.5 requiere que en la ejecución de bytecode invokestatic

la clase o interfaz que declaró el método resuelto se inicializa si esa clase o interfaz aún no se ha inicializado

Cuando Thread-2 llama a staticTarget , la clase principal StaticInit obviamente no está inicializada (ya que su inicializador estático todavía se está ejecutando). Esto significa que Thread-2 debe iniciar el procedimiento de inicialización de clase descrito en JVMS §5.5 . Según este procedimiento,

  1. Si el objeto Clase para C indica que la inicialización está en progreso para C por algún otro subproceso, entonces libere el LC y bloquee el subproceso actual hasta que se le informe de que la inicialización en curso ha finalizado

Sin embargo, Thread-2 no se bloquea a pesar de que la clase está en curso de inicialización por el hilo main .

¿Qué pasa con otras JVMs?

Probé OpenJ9 y JET, y ambos se espera un punto muerto en la prueba anterior.
Es interesante que HotSpot también se cuelga en modo -Xcomp , pero no en modo -Xint o mixto.

Como sucede

Cuando el intérprete se encuentra por primera vez con el invokestatic , llama al tiempo de ejecución de JVM para resolver la referencia del método. Como parte de este proceso, JVM inicializa la clase si es necesario. Después de una resolución exitosa, el método resuelto se guarda en la entrada Caché de la agrupación constante. Constant Pool Cache es una estructura específica de HotSpot que almacena valores de pool constantes resueltos.

En la prueba anterior, el bytecode invokestatic que llama a staticTarget se resuelve primero por el hilo main . El tiempo de ejecución de intérprete omite la inicialización de la clase, porque la clase ya está siendo inicializada por el mismo hilo. El método resuelto se guarda en el caché de la agrupación constante. La próxima vez que Thread-2 ejecuta la misma invokestatic , el intérprete ve que el bytecode ya está resuelto y utiliza una entrada de caché de grupo constante sin llamar al tiempo de ejecución y, por lo tanto, omite la inicialización de clase.

Un error similar para getstatic / putstatic se corrigió hace mucho tiempo - JDK-4493560 , pero la solución no se tocó invokestatic . He enviado el nuevo error JDK-8215634 para solucionar este problema.

En cuanto al ejemplo original,

si se cuelga o no depende de qué hilo resuelve primero la llamada estática. Si es el hilo main , el programa se completa sin un interbloqueo. Si la llamada estática es resuelta por uno de los hilos de ForkJoinPool , el programa se bloquea.

Actualizar

El error está confirmed . Se soluciona en las próximas versiones: JDK 8u201, JDK 11.0.2 y JDK 12.