sistemas recuperacion que operativos mutuo mortal interbloqueos interbloqueo informatica espera deteccion bloqueo banquero algoritmo abrazo java java-8 deadlock java-stream fork-join

java - recuperacion - interbloqueos oracle



¿Por qué el flujo paralelo con lambda en el inicializador estático causa un punto muerto? (3)

Me encontré con una situación extraña en la que usar una transmisión paralela con un lambda en un inicializador estático lleva una eternidad sin uso de CPU. Aquí está el código:

class Deadlock { static { IntStream.range(0, 10000).parallel().map(i -> i).count(); System.out.println("done"); } public static void main(final String[] args) {} }

Esto parece ser un caso de prueba de reproducción mínima para este comportamiento. Si yo:

  • poner el bloque en el método principal en lugar de un inicializador estático,
  • eliminar la paralelización, o
  • quitar la lambda

El código se completa al instante. ¿Alguien puede explicar este comportamiento? ¿Es un error o está destinado?

Estoy usando OpenJDK versión 1.8.0_66-internal.


Encontré un informe de error de un caso muy similar ( JDK-8143380 ) que Stuart Marks cerró como "No es un problema":

Este es un punto muerto de inicialización de clase. El subproceso principal del programa de prueba ejecuta el inicializador estático de clase, que establece el indicador de progreso de inicialización para la clase; este indicador permanece establecido hasta que se completa el inicializador estático. El inicializador estático ejecuta una secuencia paralela, lo que hace que las expresiones lambda se evalúen en otros subprocesos. Esos hilos bloquean la espera de que la clase complete la inicialización. Sin embargo, el subproceso principal se bloquea a la espera de que se completen las tareas paralelas, lo que resulta en un punto muerto.

El programa de prueba debe cambiarse para mover la lógica de flujo paralelo fuera del inicializador estático de clase. Cierre como no un problema.

Pude encontrar otro informe de error de eso ( JDK-8136753 ), también cerrado como "No es un problema" por Stuart Marks:

Este es un punto muerto que se produce porque el inicializador estático de la enumeración de frutas está interactuando mal con la inicialización de la clase.

Consulte la Especificación del lenguaje Java, sección 12.4.2 para obtener detalles sobre la inicialización de la clase.

http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

Brevemente, lo que sucede es lo siguiente.

  1. El hilo principal hace referencia a la clase Fruit e inicia el proceso de inicialización. Esto establece el indicador de progreso de inicialización y ejecuta el inicializador estático en el hilo principal.
  2. El inicializador estático ejecuta un código en otro hilo y espera a que termine. Este ejemplo usa flujos paralelos, pero esto no tiene nada que ver con los flujos en sí. Ejecutar código en otro hilo por cualquier medio, y esperar a que ese código termine, tendrá el mismo efecto.
  3. El código en el otro hilo hace referencia a la clase Fruit, que verifica el indicador de inicialización en progreso. Esto hace que el otro hilo se bloquee hasta que se borre la bandera. (Vea el paso 2 de JLS 12.4.2.)
  4. El hilo principal está bloqueado esperando que el otro hilo termine, por lo que el inicializador estático nunca se completa. Dado que el indicador de inicialización en progreso no se borra hasta después de que se completa el inicializador estático, los subprocesos están en un punto muerto.

Para evitar este problema, asegúrese de que la inicialización estática de una clase se complete rápidamente, sin que otros subprocesos ejecuten código que requiere que esta clase haya completado la inicialización.

Cierre como no un problema.

Tenga en cuenta que FindBugs tiene un problema abierto para agregar una advertencia para esta situación.


Hay una excelente explicación de este problema por Andrei Pangin , fechada el 07 de abril de 2015. Está disponible here , pero está escrito en ruso (sugiero revisar las muestras de código de todos modos, son internacionales). El problema general es un bloqueo durante la inicialización de la clase.

Aquí hay algunas citas del artículo:

Según JLS , cada clase tiene un bloqueo de inicialización único que se captura durante la inicialización. Cuando otro subproceso intenta acceder a esta clase durante la inicialización, se bloqueará en el bloqueo hasta que se complete la inicialización. Cuando las clases se inicializan simultáneamente, es posible obtener un punto muerto.

Escribí un programa simple que calcula la suma de los enteros, ¿qué debería imprimir?

public class StreamSum { static final int SUM = IntStream.range(0, 100).parallel().reduce((n, m) -> n + m).getAsInt(); public static void main(String[] args) { System.out.println(SUM); } }

Ahora elimine parallel() o reemplace lambda con Integer::sum call: ¿qué cambiará?

Aquí vemos un punto muerto de nuevo [hubo algunos ejemplos de puntos muertos en los inicializadores de clase previamente en el artículo]. Debido a las operaciones de flujo parallel() que se ejecutan en un grupo de subprocesos separado. Estos subprocesos intentan ejecutar el cuerpo lambda, que está escrito en bytecode como un método private static dentro de la clase StreamSum . Pero este método no se puede ejecutar antes de completar el inicializador estático de clase, que espera los resultados de la finalización de la secuencia.

Lo que es más alucinante: este código funciona de manera diferente en diferentes entornos. Funcionará correctamente en una sola máquina con CPU y probablemente se bloqueará en una máquina con múltiples CPU. Esta diferencia proviene de la implementación del grupo Fork-Join. Puede verificarlo usted mismo cambiando el parámetro -Djava.util.concurrent.ForkJoinPool.common.parallelism=N


Para aquellos que se preguntan dónde están los otros hilos que hacen referencia a la clase Deadlock , las lambdas de Java se comportan como usted escribió esto:

public class Deadlock { public static int lambda1(int i) { return i; } static { IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() { @Override public int applyAsInt(int operand) { return lambda1(operand); } }).count(); System.out.println("done"); } public static void main(final String[] args) {} }

Con las clases anónimas regulares no hay punto muerto:

public class Deadlock { static { IntStream.range(0, 10000).parallel().map(new IntUnaryOperator() { @Override public int applyAsInt(int operand) { return operand; } }).count(); System.out.println("done"); } public static void main(final String[] args) {} }