L'extrait de code suivant exécute deux threads, l'un est un simple enregistrement de minuterie toutes les secondes, le second est une boucle infinie qui exécute une opération de reste:
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);
}
}
}
}
Cela donne le résultat suivant:
[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
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 paramètres, rien n'a fonctionné.
Si vous avez des suggestions pour résoudre ce problème (y compris la modification des paramètres de changement de contexte du système d'exploitation), veuillez me le faire savoir.
java
multithreading
kms333
la source
la source
-XX:+PrintCompilation
j'obtiens ce qui suit à la fin du délai prolongé: TestBlockingThread :: lambda $ 0 @ 2 (24 octets) COMPILE SKIPPED: boucle infinie triviale (réessayer à un niveau différent)-Djava.compiler=NONE
, cela ne se produira pas.Réponses:
Après toutes les explications ici (grâce à Peter Lawrey ), nous avons constaté que la source principale de cette pause est que le point de sécurité à l'intérieur de la boucle est atteint assez rarement, il faut donc beaucoup de temps pour arrêter tous les threads pour le remplacement du code compilé par JIT.
Mais j'ai décidé d'aller plus loin et de découvrir pourquoi le point de sécurité est rarement atteint. J'ai trouvé un peu déroutant pourquoi le saut arrière de la
while
boucle n'est pas "sûr" dans ce cas.Alors j'invoque
-XX:+PrintAssembly
dans toute sa gloire pour aiderAprès quelques recherches, j'ai trouvé qu'après la troisième recompilation du
C2
compilateur lambda , les sondages safepoint étaient complètement rejetés dans la boucle.METTRE À JOUR
Au cours de l'étape de profilage, la variable
i
n'a jamais été vue égale à 0. C'est pourquoiC2
optimisé de manière spéculative cette branche, de sorte que la boucle a été transformée en quelque chose commeNotez qu'à l'origine la boucle infinie a été remodelée en une boucle finie régulière avec un compteur! En raison de l'optimisation JIT pour éliminer les sondages de point de sécurité dans les boucles comptées finies, il n'y avait pas non plus de sondage de point de sécurité dans cette boucle.
Après un certain temps,
i
je suis retourné0
et le piège rare a été pris. La méthode a été désoptimisée et son exécution s'est poursuivie dans l'interprète. Lors de la recompilation avec une nouvelle connaissanceC2
reconnue la boucle infinie et abandonné la compilation. Le reste de la méthode s'est déroulé dans l'interprète avec des points de sécurité appropriés.Il existe un excellent article de blog à lire absolument "Safepoints: Signification, effets secondaires et frais généraux" de Nitsan Wakart couvrant les points de sécurité et ce problème particulier.
L'élimination des points de sécurité dans des boucles comptées très longues est connue pour être un problème. Le bogue
JDK-5014723
(merci à Vladimir Ivanov ) résout ce problème.La solution de contournement est disponible jusqu'à ce que le bogue soit finalement corrigé.
-XX:+UseCountedLoopSafepoints
(il va faire pénalité de performance globale et peut conduire à l' accident JVMJDK-8161147
). Après avoir utilisé leC2
compilateur, continuez à garder les points de sécurité aux sauts arrière et la pause d'origine disparaît complètement.Vous pouvez désactiver explicitement la compilation de la méthode problématique en utilisant
-XX:CompileCommand='exclude,binary/class/Name,methodName'
Ou vous pouvez réécrire votre code en ajoutant manuellement un point de restauration. Par exemple, un
Thread.yield()
appel à la fin du cycle ou même le passageint i
àlong i
(merci, Nitsan Wakart ) corrigera également la pause.la source
-XX:+UseCountedLoopSafepoints
en production, car cela pourrait planter JVM . La meilleure solution de contournement à ce jour consiste à diviser manuellement la longue boucle en une boucle plus courte.c2
supprime les points de restauration! mais une autre chose que je n'ai pas obtenue est ce qui se passe ensuite. pour autant que je puisse voir, il ne reste plus de points de sécurité après le déroulement de la boucle (?) et il semble qu'il n'y ait aucun moyen de faire du stw. il y a donc une sorte de délai d'attente et une désoptimisation a lieu?i
n'est jamais égal à 0, donc la boucle est transformée de façon spéculative en quelque chose commefor (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();
une boucle comptée finie régulière. Une foisi
retourné à 0, le piège inhabituel est pris, la méthode est désoptimisée et poursuivie dans l'interpréteur. Lors de la recompilation avec les nouvelles connaissances, JIT reconnaît la boucle infinie et abandonne la compilation. Le reste de la méthode est exécuté dans l'interpréteur avec des points de sécurité appropriés.En bref, la boucle que vous avez n'a pas de point sûr à l'intérieur, sauf lorsqu'elle
i == 0
est atteinte. Lorsque cette méthode est compilée et déclenche le remplacement du code, elle doit amener tous les threads à un point sûr, mais cela prend beaucoup de temps, bloquant 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.
J'ai également modifié le code pour utiliser la virgule flottante, ce qui semble prendre plus de temps.
Et ce que je vois dans la sortie est
Remarque: pour que le code soit remplacé, les threads doivent être arrêtés à un point sûr. Cependant, il apparaît ici qu'un tel point de sécurité est atteint très rarement (peut-être uniquement lors du
i == 0
changement de la tâche enJe vois un retard similaire.
En ajoutant du code à la boucle avec précaution, vous obtenez un délai plus long.
obtient
Cependant, modifiez le code pour utiliser une méthode native qui a toujours un point de sécurité (si ce n'est pas un intrinsèque)
impressions
Remarque: l'ajout
if (Thread.currentThread().isInterrupted()) { ... }
à une boucle ajoute un point sûr.Remarque: cela s'est produit sur une machine à 16 cœurs, il n'y a donc pas de manque de ressources CPU.
la source
J'ai trouvé la réponse pour savoir pourquoi . Ils sont appelés safepoints et sont mieux connus sous le nom de Stop-The-World qui se produit à cause de GC.
Voir cet article: Journalisation des pauses stop-the-world dans JVM
En lisant le glossaire des termes HotSpot , il définit ceci:
En cours d'exécution avec les indicateurs mentionnés ci-dessus, j'obtiens cette sortie:
Notez le troisième événement STW:
Temps total d'arrêt: 10,7951187 secondes L'
arrêt des threads a pris: 10,7950774 secondes
JIT lui-même n'a pratiquement pas pris de temps, mais une fois que la JVM a décidé d'effectuer une compilation JIT, elle est entrée en mode STW, mais comme le code à compiler (la boucle infinie) n'a pas de site d'appel , aucun point de sécurité n'a jamais été atteint.
Le STW se termine lorsque JIT abandonne finalement l'attente et conclut que le code est dans une boucle infinie.
la source
Après avoir suivi les fils de commentaires et quelques tests par moi-même, je pense que la pause est causée par le compilateur JIT. Pourquoi le compilateur JIT prend si longtemps est au-delà de ma capacité à déboguer.
Cependant, puisque vous avez seulement demandé comment éviter cela, j'ai une solution:
Tirez votre boucle infinie dans une méthode où elle peut être exclue du compilateur JIT
Exécutez votre programme avec cet argument VM:
-XX: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (remplacez PACKAGE par les informations de votre package)
Vous devriez recevoir un message comme celui-ci pour indiquer quand la méthode aurait été compilée par JIT:
### Excluant compile: static blocking.TestBlockingThread :: infLoop
vous remarquerez peut-être que j'ai mis la classe dans un package appelé blocking
la source
i == 0
while
boucle n'est pas un point de sécurité?if (i != 0) { ... } else { safepoint(); }
mais c'est très rare. c'est à dire. si vous quittez / rompez la boucle, vous obtenez à peu près les mêmes horaires.