Poniższy fragment kodu wykonuje dwa wątki, jeden to prosty licznik czasu rejestrujący co sekundę, drugi to nieskończona pętla, która wykonuje pozostałą operację:
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ątków i inne ustawienia, nic nie działało.
Jeśli masz jakieś sugestie, jak to naprawić (w tym zmiana ustawień przełączania kontekstu systemu operacyjnego), daj mi znać.
java
multithreading
kms333
źródło
źródło
-XX:+PrintCompilation
, w momencie zakończenia przedłużonego opóźnienia otrzymuję następujące informacje: TestBlockingThread :: lambda $ 0 @ 2 (24 bajty) COMPILE SKIPPED: trywialna nieskończona pętla (ponowna próba na innym poziomie)-Djava.compiler=NONE
, to się nie stanie.Odpowiedzi:
Po wszystkich wyjaśnieniach tutaj (dzięki Peter Lawrey ) odkryliśmy, że głównym źródłem tej przerwy jest to, że punkt bezpieczeństwa wewnątrz pętli jest osiągany raczej rzadko, więc zatrzymanie wszystkich wątków w celu zastąpienia kodu skompilowanego w JIT zajmuje dużo czasu.
Ale zdecydowałem się pójść głębiej i dowiedzieć się, dlaczego rzadko osiąga się bezpieczny punkt. Wydało mi się trochę zagmatwane, dlaczego skok
while
pętli w tył nie jest w tym przypadku „bezpieczny”.Wzywam więc do pomocy
-XX:+PrintAssembly
w całej okazałościPo pewnym dochodzeniu odkryłem, że po trzeciej rekompilacji lambda
C2
kompilator całkowicie wyrzucił ankiety punktu bezpieczeństwa w pętli.AKTUALIZACJA
Na etapie profilowania zmienna
i
nigdy nie była równa 0. Dlatego teżC2
spekulacyjnie zoptymalizowano tę gałąź tak, aby pętla została przekształcona w coś podobnegoZauważ, że pierwotnie nieskończona pętla została przekształcona w regularną skończoną pętlę z licznikiem! Ze względu na optymalizację JIT w celu wyeliminowania sondaży punktów bezpieczeństwa w pętlach o skończonej liczbie zliczania, w tej pętli nie było również sondowania punktów bezpieczeństwa.
Po jakimś czasie
i
zawinięty z powrotem0
i niecodzienna pułapka została złapana. Metoda została odoptymalizowana 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 punktami bezpieczeństwa.Jest świetny wpis na blogu, który trzeba przeczytać „Safepoints: Meaning, Side Effects and Overheads” autorstwa Nitsana Wakarta i ten szczególny problem.
Wiadomo, że problemem jest eliminacja punktu bezpiecznego w bardzo długich zliczanych pętlach. Błąd
JDK-5014723
(dzięki Vladimir Ivanov ) rozwiązuje ten problem.Obejście jest dostępne do czasu ostatecznego naprawienia błędu.
-XX:+UseCountedLoopSafepoints
( spowoduje to ogólny spadek wydajności i może doprowadzić do awarii JVMJDK-8161147
). Po jego użyciuC2
kompilator kontynuuje przechowywanie punktów bezpieczeństwa z tyłu, skacze i oryginalna pauza znika całkowicie.Możesz jawnie wyłączyć kompilację problematycznej metody przy użyciu
-XX:CompileCommand='exclude,binary/class/Name,methodName'
Możesz też przepisać swój kod, dodając ręcznie bezpieczny punkt. Na przykład
Thread.yield()
połączenie na koniec cyklu lub nawet zmianaint i
nalong i
(dzięki, Nitsan Wakart ) również naprawi pauzę.źródło
-XX:+UseCountedLoopSafepoints
w środowisku produkcyjnym, ponieważ może to spowodować awarię JVM . Jak dotąd najlepszym obejściem jest ręczne podzielenie długiej pętli na krótsze.c2
usuwa punkty bezpieczeństwa! ale jeszcze jedna rzecz, której nie dostałem, to to, co będzie dalej. o ile widzę, po rozwinięciu pętli nie ma żadnych punktów bezpieczeństwa (?) i wygląda na to, że nie ma sposobu na zrobienie stw. więc występuje jakiś limit czasu i następuje deoptymalizacja?i
nigdy nie ma wartości 0, więc pętla jest spekulatywnie przekształcana w coś takiego, jak np.for (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();
Zwykła skończona pętla liczona. Poi
powrocie do 0, niezwykła pułapka zostaje podjęta, metoda jest deoptymalizowana i kontynuowana w interpretatorze. Podczas rekompilacji z nową wiedzą JIT rozpoznaje nieskończoną pętlę i rezygnuje z kompilacji. Pozostała część metody jest wykonywana w tłumaczu z odpowiednimi punktami bezpieczeństwa.Krótko mówiąc, pętla, którą masz, nie ma w sobie bezpiecznego punktu, z wyjątkiem sytuacji, gdy
i == 0
zostanie osiągnięta. Gdy ta metoda jest kompilowana i wyzwala 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, w którym działa kod, ale wszystkie wątki w JVM.Dodałem następujące opcje wiersza poleceń.
Zmodyfikowałem również kod, aby używał zmiennoprzecinkowych, co wydaje się trwać dłużej.
I to, co widzę na wyjściu to
Uwaga: aby zastąpić kod, wątki muszą zostać zatrzymane w bezpiecznym miejscu. Okazuje się jednak, że taki bezpieczny punkt osiąga się bardzo rzadko (być może tylko przy
i == 0
zmianie zadania naWidzę podobne opóźnienie.
Dodając kod do pętli ostrożnie, uzyskujesz większe opóźnienie.
dostaje
Jednak zmień kod, aby używał metody natywnej, która zawsze ma bezpieczny punkt (jeśli nie jest wewnętrzna)
wydruki
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.
źródło
Znalazłem odpowiedź, dlaczego . Nazywa się je bezpiecznymi punktami i są najlepiej znane jako Stop-The-World, który ma miejsce z powodu GC.
Zobacz te artykuły: Rejestrowanie przerw stop-the-world w JVM
Czytając Słowniczek terminów HotSpot , definiuje to:
Działając z wyżej wymienionymi flagami, otrzymuję taki wynik:
Zwróć uwagę na trzecie zdarzenie STW:
Całkowity czas zatrzymania: 10,7951187 sekund
Zatrzymywanie wątków trwało: 10,7950774 sekund
Sam JIT praktycznie nie zajmował czasu, ale gdy JVM zdecydował się wykonać kompilację JIT, przeszedł w tryb STW, jednak ponieważ kod do kompilacji (nieskończona pętla) nie ma miejsca wywołania , nigdy nie osiągnięto punktu bezpiecznego.
STW kończy się, gdy JIT ostatecznie rezygnuje z czekania i stwierdza, że kod jest w nieskończonej pętli.
źródło
Po prześledzeniu wątków komentarzy i kilku testach we własnym zakresie uważam, że pauza jest spowodowana przez kompilator JIT. Dlaczego kompilator JIT trwa tak długo, wykracza poza moje możliwości debugowania.
Ponieważ jednak zapytałeś tylko, jak temu zapobiec, mam rozwiązanie:
Przeciągnij nieskończoną pętlę do metody, w której można ją wykluczyć z kompilatora JIT
Uruchom swój program z tym argumentem VM:
-XX: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (zamień PACKAGE na informacje o swoim pakiecie)
Powinieneś otrzymać taki komunikat, aby wskazać, kiedy metoda została skompilowana w JIT:
### Z wyłączeniem compile: static blocking.TestBlockingThread :: infLoop
Możesz zauważyć, że umieściłem klasę w pakiecie o nazwie blocking
źródło
i == 0
while
pętli nie jest bezpiecznym punktem?if (i != 0) { ... } else { safepoint(); }
ale jest to bardzo rzadkie. to znaczy. jeśli wyjdziesz / przerwiesz pętlę, uzyskasz takie same czasy.