web-dev-qa-db-fra.com

Java Le thread 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 un simple chronomètre enregistrant 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(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 de fil et d'autres paramètres, rien n'a fonctionné.

Si vous avez des suggestions pour résoudre ce problème (notamment en peaufinant les paramètres de changement de contexte de système d'exploitation), veuillez me le faire savoir.

122
kms333

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 toutes les discussions pour JIT- remplacement du code compilé.

Mais j'ai décidé d'aller plus loin et trouver pourquoi safepoint est atteint rarement. J'ai trouvé un peu déroutant pourquoi le saut en arrière de la boucle while n'est pas "sûr" dans ce cas.

J'appelle 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 une enquête, j’ai trouvé qu’après la troisième recompilation de lambda C2, Le compilateur avait jeté complètement les sondages de points de sécurité dans la boucle.

[~ # ~] met à jour [~ # ~]

Pendant la phase de profilage, la variable i n'a jamais été vue égale à 0. C'est pourquoi C2 A optimisé cette branche de façon spéculative, de sorte que la boucle soit 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 infinie à l’origine était transformée en une boucle finie régulière avec un compteur! En raison de l'optimisation de JIT visant à éliminer les scrutations par safepoint dans les boucles à comptages finis, il n'y avait pas non plus de scrutations à safepoint dans cette boucle.

Après un certain temps, i est renvoyé à 0, Et le piège inhabituel a été pris. La méthode a été désoptimisée et son exécution poursuivie dans l’interprète. Lors de la recompilation avec de nouvelles connaissances, C2 A reconnu la boucle infinie et a 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 overheads" by Nitsan Wakart couvrant les safepoints et ce problème particulier.

L'élimination des points de sécurité dans les très longues boucles comptées est connue pour être un problème. Le bug JDK-5014723 (grâce à Vladimir Ivanov ) résout ce problème.

La solution de contournement est disponible jusqu'à ce que le bogue soit finalement résolu.

  1. Vous pouvez essayer d’utiliser -XX:+UseCountedLoopSafepoints (it will cause une pénalité de performance globale et peut entraîner un blocage 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 originale disparaît complètement.
  2. Vous pouvez explicitement désactiver la compilation de la méthode problématique en utilisant
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. Ou vous pouvez réécrire votre code en ajoutant manuellement safepoint. Par exemple, Thread.yield() appel en fin de cycle ou même changer int i En long i (Merci, Nitsan Wakart ) corrigera également la pause.

93
vsminkov

En bref, la boucle que vous avez n’a pas de point de sécurité sauf si i == 0 Est atteint. Lorsque cette méthode est compilée et que le code à remplacer est déclenché, il doit amener tous les threads à un point sûr, mais cela prend beaucoup de temps et ne verrouille pas uniquement le thread qui exécute 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 la virgule flottante, ce 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 à un point sûr. Cependant, il apparaît ici qu’un tel point sécurisé est très rarement atteint (éventuellement 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 le code à la boucle avec précaution, 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 de sécurité (si ce n’est pas intrinsèque)

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

empreintes

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

Remarque: ajouter if (Thread.currentThread().isInterrupted()) { ... } à une boucle ajoute un point sécurisé.

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

64
Peter Lawrey

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 grâce au GC.

Voir ces articles: La journalisation marque une pause dans la JVM

Différents événements peuvent amener la machine virtuelle Java à mettre tous les threads d'application en pause. Ces pauses sont appelées pauses Stop-The-World (STW). La cause la plus courante du déclenchement d'une pause STW est le garbage collection (exemple dans github), mais différentes actions JIT (exemple), la révocation biaisée du verrou ( exemple), certaines opérations JVMTI et bien d’autres nécessitent également 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 du GC soient activés. Cependant, cela ne capture pas les informations sur tous les points de sécurité. Pour tout avoir, utilisez les options de la machine virtuelle Java suivantes:

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

Si vous vous interrogez sur le nom faisant explicitement référence à 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 place. Si vous exécutez un exemple suivant (source dans github) avec les indicateurs spécifiés ci-dessus.

En lisant le Glossaire de HotSpot , il définit ceci:

safepoint

Un point pendant l'exécution du programme où toutes les racines du GC sont connues et tous les contenus d'objet de tas sont cohérents. D'un point de vue global, tous les threads doivent bloquer à un point de sécurité avant que le CPG puisse s'exécuter. (Dans le cas particulier, les threads exécutant du code JNI peuvent continuer à s'exécuter, car ils n'utilisent que des handles. Au cours d'un safepoint, ils doivent bloquer au lieu de charger le contenu du handle.) Du point de vue local, un safepoint est un point distingué. dans un bloc de code où le processus d'exécution peut bloquer pour le GC. La plupart des sites d'appels sont considérés comme des safepoints. Il existe de puissants invariants qui sont vérifiés à chaque safeppoint et qui peuvent être ignorés pour les non-safepoints. Le code compilé Java et le code C/C++ doivent être optimisés entre des safepoints, mais moins entre des safepoints. Le compilateur JIT émet une carte GC à chaque safepoint. Code C/C++ dans le VM utilise des conventions stylisées basées sur des macros (par exemple, TRAPS) pour marquer des points de sécurité potentiels.

En utilisant les drapeaux mentionnés ci-dessus, je reçois 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
L'arrêt des discussions a pris: 10.7950774 secondes

JIT ne prenait pratiquement pas de temps, mais une fois que la machine virtuelle a décidé d’effectuer une compilation JIT, elle est entrée en mode STW. Toutefois, puisque le code à compiler (la boucle infinie) n’a pas de appel site, aucun safepoint 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.

26
Andreas

Après avoir suivi les discussions de commentaires et effectué quelques tests, je pense que la pause est provoquée par le compilateur JIT. Pourquoi le compilateur JIT prend autant de temps dépasse ma capacité à déboguer.

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

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

public class TestBlockingThread {
    private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName());

    public static final void main(String[] 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: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (remplace 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:
### Hors compile: blocage statique.TestBlockingThread :: infLoop
vous remarquerez peut-être que je mets la classe dans un paquetage appelé blocage

5
Jeutnarg