122 votes

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 fils, l'un est un simple minuteur de journalisation chaque seconde, la seconde est une boucle infinie qui exécute une opération modulo:

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 l'infini bloque tous les autres threads 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 (y compris les peaufiner OS commutation de contexte paramètres) s'il vous plaît laissez-moi savoir.

93voto

vsminkov Points 7564

Après toutes les explications ici (merci à Peter Lawrey), nous avons trouvé que la principale source de cette pause est qu'un point de restauration à l'intérieur de la boucle est atteint que très rarement donc ça prend du temps pour arrêter tous les threads pour JIT-code compilé de remplacement.

Mais j'ai décidé d'aller plus loin et de trouver pourquoi le point de restauration est atteint rarement. Je l'ai trouvé un peu confus, pourquoi le saut arrière de l' while boucle n'est pas "safe" dans ce cas.

J'ai donc invoquer -XX:+PrintAssembly dans toute sa gloire de vous aider

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

Après quelques recherches, j'ai trouvé que, après la troisième recompilation de lambda C2 compilateur jeté un point de restauration des sondages à l'intérieur de la boucle complètement.

Mise à JOUR

Au cours de l'étape de profilage variable i n'a jamais été considéré égal à 0. C'est pourquoi, C2 la spéculation optimisé cette branche là, de sorte que la boucle a été transformé en quelque chose comme

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

Notez qu'à l'origine, la boucle infinie a été remodelé à un régulière boucle finie avec un compteur! En raison de JIT, l'optimisation pour éliminer un point de restauration des sondages dans le fini compté boucles, il n'y a pas de point de restauration, des bureaux de scrutin dans cette boucle.

Après un certain temps, i enveloppé retour à l' 0, et le rare piège a été prise. La méthode a été deoptimized et la poursuite de l'exécution de l'interprète. Lors de la recompilation avec un nouveau C2 reconnu la boucle infinie et a donné jusqu'à la compilation. Le reste de la méthode de procédé de l'interprète avec le bon point de restauration.

Il y a un grand must-read post de blog "points de restauration: le Sens, les Effets Secondaires et les frais généraux" par Nitsan Wakart couvrant les points de restauration et de ce problème particulier.

Un point de restauration de l'élimination dans très longtemps compté boucles est connu 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 bug est résolu.

  1. Vous pouvez essayer d'utiliser -XX:+UseCountedLoopSafepoints (il va causer de la performance globale de pénalité et peut conduire à la JVM crash JDK-8161147). Après l'utilisation de C2 compilateur continuer à garder des points de restauration à l'arrière des sauts et original pause disparaît complètement.
  2. Vous pouvez désactiver explicitement la compilation de la problématique, en utilisant la méthode
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. Ou vous pouvez écrire votre code en ajoutant un point de restauration manuellement. Par exemple Thread.yield() à la fin de cycle ou même en changeant int i de long i (merci, Nitsan Wakart) seront également fixer la pause.

64voto

Peter Lawrey Points 229686

Bref, la boucle que vous avez n'a pas de point fort à l'intérieur, sauf lorsqu' i == 0 est atteint. Lorsque cette méthode est compilé et déclenche le code pour être remplacé, il doit apporter tous les threads d'un point fort, mais cela prend un temps très long, verrouillage non seulement le fil de l'exécution du code, mais tous les threads dans la JVM.

J'ai ajouté les options de ligne de commande.

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

J'ai aussi modifié le code pour utiliser de 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 le code pour être remplacé, threads doivent être arrêtés à un point fort. Toutefois, il apparaît ici que de tels un coffre-fort point est atteint très rarement (peut-être lors de l' i == 0 Évolution de la tâche à

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

L'ajout de code de la boucle attentivement, 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, modifier le code pour utiliser une méthode native, qui a toujours un point fort (si elle n'est pas intrinsèque)

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

imprime

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

Remarque: l'ajout d' if (Thread.currentThread().isInterrupted()) { ... } d'une boucle ajoute un point fort.

Remarque: Ce qui s'est passé sur un 16 de base de la machine n'est donc pas le manque de ressources CPU.

26voto

Andreas Points 3334

La réponse de pourquoi. Ils sont appelés des points de restauration, et sont mieux connu comme le Cessez-Le-Monde qui se produit à cause de la cg.

Voir cet article: la Journalisation de cessez-le-monde pauses dans la JVM

Différents événements peuvent causer de la JVM pour une pause tous les threads de l'application. Ces pauses sont appelés Stop-The-World (STW) met en pause. La cause la plus commune pour un STW pause pour être déclenchée est collecte des déchets (exemple sur github) , mais différents JIT actions (par exemple), biaisée de verrouillage de la révocation (par exemple), certains JVMTI opérations , et beaucoup d'autres aussi exiger que la demande soit arrêté.

Les points sur lesquels les threads de l'application peut être arrêté en toute sécurité sont appelés, surprise, des points de restauration. Ce terme est souvent utilisé pour se référer à tous les STW des pauses.

Il est plus ou moins commun que GC logs sont activés. Cependant, cela ne permet pas de capturer de l'information sur tous les points de restauration. Pour obtenir tout cela, utilisez ces options JVM:

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

Si vous vous posez des questions sur le choix des noms se référant explicitement à la GC, ne vous inquiétez pas – activation de ces options journaux de tous les points de restauration, non seulement la collecte des ordures pauses. Si vous exécutez un exemple ci-dessous (source sur github) avec les indicateurs spécifiés ci-dessus.

La lecture du HotSpot Glossaire des Termes, il définit ce:

un point de restauration

Un point pendant l'exécution du programme au cours de laquelle tous les GC racines sont connues et toutes les tas d'objet de contenu est cohérent. À partir d'un point de vue global, tous les threads doivent bloquer à un point de restauration avant le GC peut exécuter. (Comme un cas spécial, les threads s'exécutant JNI code peut continuer à fonctionner, car ils n'utilisent que des poignées. Au cours d'un point de restauration, ils doivent bloquer au lieu de charger le contenu de la poignée.) À partir d'un point de vue local, d'un point de restauration est une brillante point dans un bloc de code dans lequel l'exécution du thread peut bloquer pour la GC. La plupart des sites d'appel qualifier comme des points de restauration. Il y a de fortes invariants qui détiennent vrai à chaque point de restauration, qui ne peuvent être écartées au non des points de restauration. À la fois compilé en code Java et C/C++ code soit optimisé entre les points de restauration, mais moins dans les points de restauration. Le compilateur JIT émet un GC carte à chaque point de restauration. Code C/C++ dans la machine virtuelle utilise stylisée de la macro à base de conventions (par exemple, les PIÈGES) pour marquer le potentiel des points de restauration.

En cours d'exécution avec le ci-dessus mentionné drapeaux, j'obtiens ce résultat:

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 STW événement:
Total le temps s'est arrêté: 10.7951187 secondes
L'arrêt de threads a: 10.7950774 secondes

JIT lui-même a pris pratiquement pas de temps, mais une fois que la JVM a décidé d'effectuer une compilation JIT, il est entré STW mode, mais depuis, le code à compiler (boucle infinie) ne dispose pas d'un site d'appel, aucun point de restauration n'a jamais été atteint.

STW se termine lorsque le JIT finalement donne d'attente et conclut le code est dans une boucle infinie.

5voto

Jeutnarg Points 869

Après avoir suivi le commentaire de threads et quelques tests sur mon propre, je crois que la pause est causée par le compilateur JIT. Pourquoi le compilateur JIT est de prendre un temps long est au-delà de ma capacité à déboguer.

Cependant, puisque vous n'demandé comment faire pour éviter cela, j'ai une solution:

Tirez la boucle infinie dans une méthode où il peut être exclu de le 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écuter votre programme avec cette VM argument:

-XX:CompileCommand=exclure,PACKAGE.TestBlockingThread::infLoop (remplacer le PAQUET avec votre package d'informations)

Vous devriez obtenir un message comme celui-ci pour indiquer lorsque la méthode aurait été compilé par JIT:
### À l'exclusion de la compilation: statique blocage.TestBlockingThread::infLoop
vous pouvez remarquer que j'ai mis la classe dans un package appelé blocage

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X