El siguiente fragmento de código ejecuta dos subprocesos, uno es un temporizador simple que registra cada segundo, el segundo es un bucle infinito que ejecuta una operación restante:
public class TestBlockingThread {
private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);
public static final void main(String[] args) throws InterruptedException {
Runnable task = () -> {
int i = 0;
while (true) {
i++;
if (i != 0) {
boolean b = 1 % i == 0;
}
}
};
new Thread(new LogTimer()).start();
Thread.sleep(2000);
new Thread(task).start();
}
public static class LogTimer implements Runnable {
@Override
public void run() {
while (true) {
long start = System.currentTimeMillis();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// do nothing
}
LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
}
}
}
}
Esto da el siguiente resultado:
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
No entiendo por qué la tarea infinita bloquea todos los otros hilos durante 13,3 segundos. Traté de cambiar las prioridades del hilo y otras configuraciones, nada funcionó.
Si tiene alguna sugerencia para solucionar esto (incluida la modificación de la configuración de cambio de contexto del sistema operativo), hágamelo saber.
java
multithreading
kms333
fuente
fuente
-XX:+PrintCompilation
, obtengo lo siguiente en el momento en que finaliza el retraso extendido: TestBlockingThread :: lambda $ 0 @ 2 (24 bytes) COMPILE SKIPPED: bucle infinito trivial (reintento en diferentes niveles)-Djava.compiler=NONE
, no sucederá.Respuestas:
Después de todas las explicaciones aquí (gracias a Peter Lawrey ) descubrimos que la fuente principal de esta pausa es que el punto seguro dentro del bucle se alcanza con bastante poca frecuencia, por lo que lleva mucho tiempo detener todos los hilos para el reemplazo del código compilado JIT.
Pero decidí profundizar y descubrir por qué raramente se alcanza el punto seguro. Me pareció un poco confuso por qué el salto hacia atrás del
while
bucle no es "seguro" en este caso.Entonces convoco
-XX:+PrintAssembly
en toda su gloria para ayudarDespués de un poco de investigación, descubrí que después de una tercera recopilación del
C2
compilador lambda , descarté completamente las encuestas de punto seguro dentro del bucle.ACTUALIZAR
Durante la etapa de perfil, la variable
i
nunca se vio igual a 0. Es por eso queC2
optimizó especulativamente esta rama, de modo que el bucle se transformó en algo así como¡Tenga en cuenta que el bucle infinito original se reformó en un bucle finito regular con un contador! Debido a la optimización JIT para eliminar las encuestas de punto seguro en bucles contados finitos, tampoco hubo encuesta de punto seguro en este bucle.
Después de un tiempo,
i
regresó0
y la trampa poco común fue tomada. El método se desoptimizó y se siguió ejecutando en el intérprete. Durante laC2
compilación con un nuevo conocimiento reconoció el bucle infinito y abandonó la compilación. El resto del método procedió en el intérprete con los puntos de seguridad adecuados.Hay una gran publicación de blog que debe leerse "Safepoints: significado, efectos secundarios y gastos generales" por Nitsan Wakart que cubre los puntos seguros y este tema en particular.
Se sabe que la eliminación de puntos seguros en bucles contados muy largos es un problema. El error
JDK-5014723
(gracias a Vladimir Ivanov ) soluciona este problema.La solución alternativa está disponible hasta que finalmente se solucione el error.
-XX:+UseCountedLoopSafepoints
(que va a causar pena de actuación global y puede conducir a bloqueo de JVMJDK-8161147
). Después de usar elC2
compilador, continúe manteniendo los puntos seguros en los saltos hacia atrás y la pausa original desaparecerá por completo.Puede deshabilitar explícitamente la compilación del método problemático utilizando
-XX:CompileCommand='exclude,binary/class/Name,methodName'
O puede reescribir su código agregando Safepoint manualmente. Por ejemplo,
Thread.yield()
llamar al final del ciclo o incluso cambiarint i
along i
(gracias, Nitsan Wakart ) también reparará la pausa.fuente
-XX:+UseCountedLoopSafepoints
en producción, ya que puede bloquear JVM . La mejor solución hasta ahora es dividir manualmente el bucle largo en otros más cortos.c2
elimina los puntos seguros! pero una cosa más que no entendí es lo que sigue. Por lo que puedo ver, no quedan puntos seguros después de desenrollar el bucle (?) y parece que no hay forma de hacer stw. Entonces, ¿se produce algún tipo de tiempo de espera y se produce la optimización?i
nunca es 0, por lo que el bucle se transforma especulativamente en algo así comofor (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();
un bucle contado finito regular. Una vez quei
vuelve a 0, se toma la trampa poco común, el método se desoptimiza y se procede al intérprete. Durante la compilación con el nuevo conocimiento, JIT reconoce el bucle infinito y abandona la compilación. El resto del método se ejecuta en un intérprete con puntos de seguridad adecuados.En resumen, el bucle que tiene no tiene un punto seguro dentro, excepto cuando
i == 0
se alcanza. Cuando este método se compila y activa el código que se debe reemplazar, debe llevar todos los subprocesos a un punto seguro, pero esto lleva mucho tiempo, bloqueando no solo el subproceso que ejecuta el código sino todos los subprocesos en la JVM.Agregué las siguientes opciones de línea de comando.
También modifiqué el código para usar el punto flotante que parece tomar más tiempo.
Y lo que veo en la salida es
Nota: para que el código sea reemplazado, los hilos deben detenerse en un punto seguro. Sin embargo, parece que aquí rara vez se alcanza un punto seguro (posiblemente solo cuando se
i == 0
cambia la tarea aVeo un retraso similar.
Si agrega código al bucle con cuidado, obtendrá un retraso mayor.
consigue
Sin embargo, cambie el código para usar un método nativo que siempre tenga un punto seguro (si no es intrínseco)
huellas dactilares
Nota: agregar
if (Thread.currentThread().isInterrupted()) { ... }
a un bucle agrega un punto seguro.Nota: Esto sucedió en una máquina de 16 núcleos, por lo que no faltan recursos de CPU.
fuente
Encontré la respuesta de por qué . Se llaman puntos de seguridad, y son más conocidos como Stop-The-World que sucede debido a GC.
Vea estos artículos: Registro de pausas para detener el mundo en JVM
Al leer el Glosario de términos de HotSpot , define esto:
Al ejecutar los indicadores mencionados anteriormente, obtengo este resultado:
Observe el tercer evento STW:
Tiempo total detenido: 10.7951187 segundos La
detención de subprocesos tardó: 10.7950774 segundos
JIT en sí prácticamente no tomó tiempo, pero una vez que JVM decidió realizar una compilación JIT, entró en modo STW, sin embargo, dado que el código que se compilará (el bucle infinito) no tiene un sitio de llamada , nunca se llegó a ningún punto seguro.
El STW termina cuando JIT finalmente deja de esperar y concluye que el código está en un bucle infinito.
fuente
Después de seguir los hilos de comentarios y algunas pruebas por mi cuenta, creo que la pausa es causada por el compilador JIT. Por qué el compilador de JIT está tardando tanto tiempo está más allá de mi capacidad de depurar.
Sin embargo, dado que solo preguntó cómo prevenir esto, tengo una solución:
Lleva tu bucle infinito a un método donde pueda excluirse del compilador JIT
Ejecute su programa con este argumento VM:
-XX: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (reemplace PACKAGE con la información de su paquete)
Debería recibir un mensaje como este para indicar cuándo se habría compilado el método JIT:
### Excluyendo la compilación: bloqueo estático.TestBlockingThread :: infLoop
puede notar que puse la clase en un paquete llamado bloqueo
fuente
i == 0
while
bucle no es un punto seguro?if (i != 0) { ... } else { safepoint(); }
pero esto es muy raro. es decir. si sales / rompes el ciclo, obtienes los mismos tiempos.