Java thread wykonujący resztę operacji w pętli blokuje wszystkie pozostałe wątki

Poniższy fragment kodu wykonuje dwa wątki, jeden jest prostym timerem rejestrującym co sekundę, drugi jest nieskończoną pętlą wykonującą operację pozostałą:

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);
            }
        }
    }
}

Daje to następujący wynik:

[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

Nie rozumiem, dlaczego nieskończone zadanie blokuje wszystkie inne wątki na 13,3 sekundy. Próbowałem zmienić priorytety wątku i inne ustawienia, nic nie działało.

Jeśli masz jakieś sugestie, aby to naprawić (w tym dostosowanie ustawień przełączania kontekstu systemu operacyjnego) proszę dać mi znać.

Author: Raedwald, 2016-09-02

4 answers

Po wszystkich wyjaśnieniach tutaj (Dzięki Peter Lawrey) odkryliśmy, że głównym źródłem tej pauzy jest to, że safepoint wewnątrz pętli jest osiągany raczej rzadko, więc zajmuje dużo czasu, aby zatrzymać wszystkie wątki do wymiany kodu skompilowanego JIT.

[[18]}ale postanowiłem pójść głębiej i znaleźć dlaczego safepoint jest osiągany rzadko. Uznałem to za trochę mylące, dlaczego skok w tył pętli while nie jest w tym przypadku "Bezpieczny".

Więc wzywam -XX:+PrintAssembly we wszystkich jego glory to help

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

Po pewnym śledztwie odkryłem, że po trzeciej rekompilacji lambda C2 kompilator całkowicie wyrzucił ankiety safepoint wewnątrz pętli.

UPDATE

Podczas etapu profilowania zmienna i nigdy nie była równa 0. Dlatego C2 spekulatywnie zoptymalizowano tę gałąź, tak aby pętla została przekształcona w coś w rodzaju

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

Zauważ, że pierwotnie nieskończona pętla została przekształcona w regularną pętlę skończoną z licznik! Ze względu na optymalizację JIT w celu wyeliminowania sondaży safepoint w skończonych liczonych pętlach, nie było również sondy safepoint w tej pętli.

Po pewnym czasie, i zawinął z powrotem do 0, a niezwykła pułapka została podjęta. Metoda została deoptymizowana i kontynuowana w Tłumaczu. Podczas rekompilacji z nową wiedzą C2 rozpoznał nieskończoną pętlę i zrezygnował z kompilacji. Reszta metody przebiegała w Tłumaczu z odpowiednimi safepointami.

Jest Wielki musi przeczytać blog post "Safepoints: Znaczenie, skutki uboczne i koszty ogólne" by Nitsan Wakart obejmujące safepoints i ten konkretny problem.

Eliminacja Safepoint w bardzo długich liczonych pętlach jest znana jako problem. The bug JDK-5014723 (Dzięki Vladimir Ivanov) rozwiązuje ten problem.

Obejście jest dostępne, dopóki błąd nie zostanie ostatecznie naprawiony.

  1. Możesz spróbować użyć -XX:+UseCountedLoopSafepoints (It will przyczyną ogólnej kary za wydajność i może doprowadzić do awarii JVM JDK-8161147). Po jej użyciu C2 kompilator kontynuuje trzymanie safepoints z tyłu skoków i oryginalna pauza znika całkowicie.
  2. Możesz jawnie wyłączyć kompilację problematycznej metody za pomocą
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. Możesz też przepisać kod, dodając safepoint ręcznie. Na przykład Thread.yield() wywołanie pod koniec cyklu lub nawet zmiana int i na long i (thanks, Nitsan Wakart ) naprawi również pauzę.

 93
Author: vsminkov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:46:36

Krótko mówiąc, pętla, którą masz, nie ma bezpiecznego punktu w środku, chyba że i == 0 zostanie osiągnięta. Gdy ta metoda jest skompilowana i uruchamia kod do zastąpienia, musi doprowadzić wszystkie wątki do bezpiecznego punktu, ale zajmuje to bardzo dużo czasu, blokując nie tylko wątek obsługujący kod, ale wszystkie wątki w JVM.

Dodałem następujące opcje wiersza poleceń.

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

Zmodyfikowałem również kod, aby używał zmiennoprzecinkowego, co wydaje się trwać dłużej.

boolean b = 1.0 / i == 0;

I to co widzę w wyjściu to

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

Uwaga: Aby kod mógł zostać zastąpiony, wątki muszą zostać zatrzymane w bezpiecznym punkcie. Wydaje się jednak, że taki bezpieczny punkt jest osiągany bardzo rzadko (być może tylko wtedy, gdy i == 0 zmienia zadanie na

Runnable task = () -> {
    for (int i = 1; i != 0 ; i++) {
        boolean b = 1.0 / i == 0;
    }
};
Widzę podobne opóźnienie.
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

Dodawanie kodu do pętli ostrożnie daje większe opóźnienie.

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

Gets

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

Jednak Zmień kod na natywny, który zawsze ma bezpieczny punkt (jeśli nie jest nieodłącznym elementem)

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

Druki

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

Uwaga: dodanie if (Thread.currentThread().isInterrupted()) { ... } do pętli dodaje bezpieczny punkt.

Uwaga: stało się to na 16-rdzeniowej maszynie, więc nie brakuje zasobów procesora.

 64
Author: Peter Lawrey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-02 21:19:52

Znalazłem odpowiedź Dlaczego . Są one nazywane safepoints i są najbardziej znane jako Stop-the-World, który dzieje się z powodu GC.

Zobacz ten artykuł: Logowanie stop-the-world pauzy w JVM

Różne zdarzenia mogą spowodować, że JVM zatrzyma wszystkie wątki aplikacji. Takie pauzy nazywane są pauzami Stop-the-World (STW). Najczęstszą przyczyną wywołania pauzy STW jest garbage collection (przykład w github) , ale różne akcje JIT (przykład), stronnicze odwołanie blokady (przykład), niektóre operacje JVMTI i wiele innych również wymagają zatrzymania aplikacji.

Punkty, w których wątki aplikacji mogą być bezpiecznie zatrzymane, nazywane są, surprise, safepoints. Termin ten jest również często używany w odniesieniu do wszystkich pauz STW.

Jest mniej lub bardziej powszechne, że dzienniki GC są włączone. Nie zawiera to jednak informacji o wszystkich punktach bezpieczeństwa. Aby uzyskać to wszystko, użyj tych JVM opcje:

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

Jeśli zastanawiasz się nad nazwami bezpośrednio odnoszącymi się do GC, nie obawiaj się – włączenie tych opcji rejestruje wszystkie punkty bezpieczeństwa, a nie tylko pauzę zbierania śmieci. Jeśli uruchomisz następujący przykład (źródło w github) z flagami podanymi powyżej.

Czytając HotSpot słowniczek terminów , definiuje to:

Safepoint

Punkt podczas wykonywania programu, w którym znane są wszystkie korzenie GC i cała zawartość obiektu heap jest spójna. Z globalnego punktu widzenia, wszystkie wątki muszą zablokować się w punkcie bezpieczeństwa, zanim GC może działać. (W specjalnym przypadku wątki z kodem JNI mogą nadal działać, ponieważ używają tylko uchwytów. Podczas safepoint muszą blokować zamiast ładować zawartość uchwytu.) Z lokalnego punktu widzenia, safepoint jest wyróżnionym punktem w bloku kodu, w którym wykonujący wątek może zablokować GC. większość witryn telefonicznych kwalifikuje się jako punkty bezpieczeństwa. Istnieją silne niezmienniki, które są prawdziwe w każdym safepoincie, które mogą być pomijane w non-safepoints. Zarówno skompilowany kod Java, jak i Kod C / C++ są zoptymalizowane między punktami bezpieczeństwa, ale mniej między punktami bezpieczeństwa. Kompilator JIT emituje mapę GC w każdym punkcie bezpieczeństwa. Kod C/C++ w maszynie wirtualnej wykorzystuje stylizowane konwencje makro (np. pułapki) do oznaczania potencjalnych punktów bezpieczeństwa.

Uruchamiając powyższe flagi, otrzymuję to wyjście:

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

Zwróć uwagę na trzeci STW wydarzenie:
Łączny czas: 10.7951187 sekund
zatrzymywanie wątków: 10.7950774 sekund

Samo JIT nie zajęło praktycznie żadnego czasu, ale gdy JVM zdecydował się wykonać kompilację JIT, wszedł w tryb STW, jednak ponieważ kod, który ma być skompilowany (nieskończona pętla) nie ma call site, nie osiągnięto safepoint.

STW kończy się, gdy JIT ostatecznie rezygnuje z czekania i kończy kod w nieskończonej pętli.

 26
Author: Andreas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-02 19:01:40

Po przeczytaniu wątków komentarzy i kilku testach na własną rękę, uważam, że pauza jest spowodowana przez kompilator JIT. Dlaczego kompilator JIT zajmuje tak dużo czasu jest poza moją zdolnością do debugowania.

Jednakże, ponieważ pytałeś tylko o to, jak temu zapobiec, mam rozwiązanie:

Przeciągnij swoją nieskończoną pętlę do metody, w której można ją wykluczyć z kompilatora 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;
            }
        }
    }

Uruchom program z tym argumentem maszyny wirtualnej:

- XX: CompileCommand=exclude, PACKAGE.TestBlockingThread::infLoop (zamień pakiet na informacje o pakiecie)

Powinieneś otrzymać taki komunikat, aby wskazać, kiedy metoda byłaby skompilowana JIT:
## # Excluding compile: static blocking.TestBlockingThread:: infLoop
możesz zauważyć, że umieściłem klasę w pakiecie o nazwie blokowanie

 5
Author: Jeutnarg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-02 19:00:18