Le thread Java exécutant l’opération restante dans une boucle bloque tous les autres threads

L’extrait de code suivant exécute deux threads, l’un est une simple timer se connectant chaque seconde, le second est une boucle infinie qui exécute une opération restante:

public class TestBlockingThread { private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class); public static final void main(Ssortingng[] 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); } } } } 

Cela donne le résultat suivant:

 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1004 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1003 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=13331 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1006 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1003 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1004 [Thread-0] INFO cmcconcurrent.TestBlockingThread - timeElapsed=1004 

Je ne comprends pas pourquoi la tâche infinie bloque tous les autres threads pendant 13,3 secondes. J’ai essayé de changer les priorités des threads et d’autres parameters, rien n’a fonctionné.

Si vous avez des suggestions pour résoudre ce problème (notamment en modifiant les parameters de changement de contexte du système d’exploitation), faites-le moi savoir.

Après toutes les explications ici (grâce à Peter Lawrey ), nous avons constaté que la principale source de cette pause est que le safepoint dans la boucle est atteint assez rarement, donc il faut beaucoup de temps pour arrêter tous les threads.

Mais j’ai décidé d’aller plus loin et de trouver pourquoi le safepoint est rarement atteint. J’ai trouvé un peu déroutant pourquoi le back jump de while loop n’est pas “sûr” dans ce cas.

Donc, -XX:+PrintAssembly dans toute sa splendeur pour aider

 -XX:+UnlockDiagnosticVMOptions \ -XX:+TraceClassLoading \ -XX:+DebugNonSafepoints \ -XX:+PrintCompilation \ -XX:+PrintGCDetails \ -XX:+PrintStubCode \ -XX:+PrintAssembly \ -XX:PrintAssemblyOptions=-Mintel 

Après quelques recherches, j’ai constaté qu’après la troisième recompilation de lambda C2 compilateur avait complètement abandonné les sondages safepoint à l’intérieur de la boucle.

METTRE À JOUR

Au cours de la phase d’étape de profilage, i n’ai jamais été vu égal à 0. C’est pourquoi C2 optimisé cette twig de manière spéculative, de sorte que la boucle a été transformée en quelque chose comme

 for (int i = OSR_value; i != 0; i++) { if (1 % i == 0) { uncommon_trap(); } } uncommon_trap(); 

Notez que la boucle à l’origine infinie a été remodelée en une boucle finie régulière avec un compteur! En raison de l’optimisation JIT pour éliminer les interrogations de safepoint dans les boucles à nombre fini, il n’y avait pas non plus d’interrogation de safepoint dans cette boucle.

Après un certain temps, i revenu à 0 et le piège rare a été pris. La méthode était désoptimisée et l’exécution continue dans l’interpréteur. Lors de la recompilation avec une nouvelle connaissance, C2 reconnu la boucle infinie et a abandonné la compilation. Le rest de la méthode s’est déroulé dans l’interprète avec les points de sécurité appropriés.

Il y a un excellent article de blog à lire absolument “Safepoints: Meaning, Side Effects and Overheads” par Nitsan Wakart couvrant les points de sécurité et ce problème particulier.

L’élimination des points de sécurité dans les boucles à très long comptage est connue pour être un problème. Le bug JDK-5014723 (merci à Vladimir Ivanov ) résout ce problème.

La solution de contournement est disponible jusqu’à ce que le bogue soit finalement corrigé.

  1. Vous pouvez essayer d’utiliser -XX:+UseCountedLoopSafepoints (cela entraînera une -XX:+UseCountedLoopSafepoints performance globale et peut conduire à un crash de la JVM JDK-8161147 ). Après l’avoir utilisé, le compilateur C2 continue à garder les points de sécurité à l’arrière et la pause d’origine disparaît complètement.
  2. Vous pouvez explicitement désactiver la compilation de la méthode problématique en utilisant
    -XX:ComstackCommand='exclude,binary/class/Name,methodName'

  3. Ou vous pouvez réécrire votre code en ajoutant manuellement safepoint. Par exemple, Thread.yield() appelle à la fin du cycle ou même change de long i en long i (merci, Nitsan Wakart ) va également réparer la pause.

En bref, la boucle que vous avez n’a pas de point sûr à l’intérieur, sauf lorsque i == 0 est atteint. Lorsque cette méthode est compilée et déclenche le code à remplacer, elle doit mettre tous les threads en sécurité, mais cela prend beaucoup de temps, verrouillant non seulement le thread exécutant le code mais tous les threads de la JVM.

J’ai ajouté les options de ligne de commande suivantes.

 -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation 

J’ai également modifié le code pour utiliser un virgule flottante qui semble prendre plus de temps.

 boolean b = 1.0 / i == 0; 

Et ce que je vois dans la sortie est

 timeElapsed=100 Application time: 0.9560686 seconds 41423 280 % 4 TestBlockingThread::lambda$main$0 @ -2 (27 bytes) made not entrant Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds Application time: 0.0000219 seconds Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds 41424 281 % 3 TestBlockingThread::lambda$main$0 @ 2 (27 bytes) timeElapsed=40473 41425 282 % 4 TestBlockingThread::lambda$main$0 @ 2 (27 bytes) 41426 281 % 3 TestBlockingThread::lambda$main$0 @ -2 (27 bytes) made not entrant timeElapsed=100 

Remarque: pour que le code soit remplacé, les threads doivent être arrêtés en un point sûr. Cependant, il apparaît ici que ce sharepoint sécurité est très rarement atteint (peut-être uniquement lorsque i == 0

 Runnable task = () -> { for (int i = 1; i != 0 ; i++) { boolean b = 1.0 / i == 0; } }; 

Je vois un délai similaire.

 timeElapsed=100 Application time: 0.9587419 seconds 39044 280 % 4 TestBlockingThread::lambda$main$0 @ -2 (28 bytes) made not entrant Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds Application time: 0.0000087 seconds Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds timeElapsed=38100 timeElapsed=100 

En ajoutant soigneusement du code à la boucle, vous obtenez un délai plus long.

 for (int i = 1; i != 0 ; i++) { boolean b = 1.0 / i / i == 0; } 

obtient

  Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds 

Cependant, changez le code pour utiliser une méthode native qui a toujours un point sûr (si ce n’est pas un insortingnsèque)

 for (int i = 1; i != 0 ; i++) { boolean b = Math.cos(1.0 / i) == 0; } 

estampes

 Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds 

Remarque: append if (Thread.currentThread().isInterrupted()) { ... } à une boucle ajoute un point sûr.

Remarque: Cela s’est produit sur une machine à 16 cœurs, les ressources du processeur ne manquent donc pas.

Trouvé la réponse de pourquoi . Ils s’appellent des points de sécurité et sont mieux connus sous le nom de Stop-The-World à cause du GC.

Voir cet article: Enregistrement des pauses stop-the-world dans JVM

Différents événements peuvent amener la JVM à suspendre tous les threads de l’application. Ces pauses sont appelées pauses Stop-The-World (STW). La cause la plus courante de déclenchement d’une pause STW est la récupération de mémoire (exemple dans github), mais différentes actions JIT (exemple), la révocation de verrou biaisée (exemple), certaines opérations JVMTI et bien d’autres encore nécessitent l’arrêt de l’application.

Les points auxquels les threads d’application peuvent être arrêtés en toute sécurité sont appelés, surprise, points de sécurité . Ce terme est également souvent utilisé pour désigner toutes les pauses STW.

Il est plus ou moins fréquent que les journaux GC soient activés. Cependant, cela ne permet pas de capturer des informations sur tous les points de sécurité. Pour tout obtenir, utilisez ces options JVM:

 -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime 

Si vous vous posez des questions sur la dénomination faisant explicitement référence au GC, ne vous inquiétez pas – l’activation de ces options enregistre tous les points de sécurité, pas seulement les pauses de récupération de mémoire. Si vous exécutez un exemple suivant (source dans github) avec les indicateurs spécifiés ci-dessus.

En lisant le glossaire des termes HotSpot , il définit ceci:

sharepoint sécurité

Un point pendant l’exécution du programme auquel toutes les racines du GC sont connues et tous les contenus des objects de tas sont cohérents. D’un sharepoint vue global, tous les threads doivent être bloqués sur un safepoint avant que le CPG puisse s’exécuter. (Dans un cas particulier, les threads exécutant du code JNI peuvent continuer à s’exécuter, car ils n’utilisent que des descripteurs. Au cours d’un safepoint, ils doivent bloquer au lieu de charger le contenu du descripteur.) dans un bloc de code où le thread d’exécution peut bloquer pour le GC. La plupart des sites d’appels sont qualifiés de points de sécurité. Il y a de forts invariants qui se vérifient à tous les points de sécurité, qui peuvent être ignorés lors des non-points de sécurité. Le code Java compilé et le code C / C ++ doivent être optimisés entre les points de sécurité, mais moins entre les points de sécurité. Le compilateur JIT émet une carte GC à chaque safepoint. Le code C / C ++ dans la machine virtuelle utilise des conventions basées sur des macros stylisées (par exemple, TRAPS) pour marquer des points de sécurité potentiels.

En cours d’exécution avec les drapeaux mentionnés ci-dessus, j’obtiens cette sortie:

 Application time: 0.9668750 seconds Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds timeElapsed=1015 Application time: 1.0148568 seconds Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds timeElapsed=1015 timeElapsed=1014 Application time: 2.0453971 seconds Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds timeElapsed=11732 Application time: 1.0149263 seconds Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds timeElapsed=1015 

Notez le troisième événement STW:
Temps total arrêté: 10.7951187 secondes
Arrêt des discussions a pris: 10,7950774 secondes

JIT lui-même ne prenait pratiquement pas de temps, mais une fois que la JVM avait décidé de réaliser une compilation JIT, elle était entrée en mode STW. Cependant, le code à comstackr (la boucle infinie) n’a pas de site d’appel .

Le STW se termine lorsque JIT abandonne finalement l’attente et conclut que le code est dans une boucle infinie.

Après avoir suivi les fils de commentaires et quelques tests, je pense que la pause est provoquée par le compilateur JIT. La raison pour laquelle le compilateur JIT prend trop de temps est au-delà de mes possibilités de débogage.

Cependant, comme vous avez seulement demandé comment empêcher cela, j’ai une solution:

Tirez votre boucle infinie dans une méthode où il peut être exclu du compilateur JIT

 public class TestBlockingThread { private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName()); public static final void main(Ssortingng[] args) throws InterruptedException { Runnable task = () -> { infLoop(); }; new Thread(new LogTimer()).start(); Thread.sleep(2000); new Thread(task).start(); } private static void infLoop() { int i = 0; while (true) { i++; if (i != 0) { boolean b = 1 % i == 0; } } } 

Exécutez votre programme avec cet argument VM:

-XX: ComstackCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (remplacez PACKAGE par vos informations de package)

Vous devriez recevoir un message comme celui-ci pour indiquer quand la méthode aurait été compilée avec JIT:
### Exclure la compilation: blocage statique.TestBlockingThread :: infLoop
vous pouvez remarquer que je mets la classe dans un paquet appelé blocage