1. Thread Dump i analiza stanu wątków
Thread Dump (zrzut wątków) – to migawka stanu wszystkich wątków w aplikacji w danym momencie. To jak zdjęcie grupowe wszystkich twoich wątków: kto czym się zajmuje, kto gdzie utknął, kto na kogo czeka. Thread Dump – to twoje główne narzędzie do szukania deadlocków, livelocków i innych zagadkowych zawieszeń.
Jak uzyskać Thread Dump?
Przez terminal (jstack):
Jeśli masz PID procesu Javy, wykonaj:
jstack <PID>
Polecenie wypisze w konsoli stan wszystkich wątków wraz z informacją, który w jakim stanie się znajduje i jakie monitory (blokady) trzyma.
Przez IDE (IntelliJ IDEA):
W menu „Run” → „Show Running List” → wybierz proces → „Thread Dump”.
Przez VisualVM lub JConsole:
Otwórz proces, znajdź kartę „Threads” i wykonaj zrzut stanu.
Przykład Thread Dump
Fragment zrzutu:
"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001e0c7800 nid=0x1a48 waiting for monitor entry [0x000000001f00f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
- waiting to lock <0x00000000d6d6baf8> (a java.lang.Object)
- locked <0x00000000d6d6bb08> (a java.lang.Object)
Widać tutaj, że wątek „Thread-1” jest zablokowany (BLOCKED), trzyma jeden monitor, ale czeka na inny. Jeśli widzisz kilka takich wątków, które trzymają zasób A i czekają na B, a inny wątek trzyma B i czeka na A – to klasyczny deadlock.
Stany wątków
| Status | Opis |
|---|---|
| RUNNABLE | Wątek działa lub jest gotowy do działania |
| BLOCKED | Czeka na przejęcie monitora (blokady) |
| WAITING | Czeka na notify()/notifyAll() (np. wywołane przez wait()) |
| TIMED_WAITING | Czeka z limitem czasu (np. sleep, wait(timeout)) |
| TERMINATED | Wątek zakończony |
Ważne: status RUNNABLE nie zawsze oznacza, że wątek właśnie teraz się wykonuje – oznacza jedynie gotowość do wykonania (planista JVM może nie uruchomić go natychmiast).
Jak rozpoznać, że masz deadlock?
W zrzucie kilka wątków w stanie BLOCKED, każdy czeka na monitor trzymany przez inny wątek z tego samego zestawu.
Na końcu zrzutu jstack zazwyczaj wypisuje:
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000d6d6baf8 (object 0x00000000d6d6baf8, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x00000000d6d6bb08 (object 0x00000000d6d6bb08, a java.lang.Object),
which is held by "Thread-1"
Jeśli wątki długo wiszą w BLOCKED lub WAITING – to powód do analizy.
2. Monitorowanie i profilowanie wątków
VisualVM
VisualVM – bezpłatne narzędzie wchodzące w skład większości JDK. Pozwala podłączyć się do procesu, podejrzeć stan wątków, wykonać Thread Dump, zobaczyć obciążenie CPU, aktywne i „wiszące” wątki.
Karta Threads: widać, ile wątków utworzono, ich stany i historię aktywności.
Thread Dump: przycisk „Thread Dump” wykonuje zrzut analogiczny do jstack.
Java Mission Control i Flight Recorder
Java Mission Control (JMC): zaawansowane narzędzie analizy działania JVM w czasie rzeczywistym. Pomaga badać blokady, czasy wykonania, alokacje, opóźnienia.
Java Flight Recorder (JFR): wbudowany profiler JVM zbierający zdarzenia o wątkach, blokadach, pauzach itp.
Przykład: monitorowanie blokad
W VisualVM lub JMC możesz zobaczyć, że:
- Wątek „A” jest zablokowany na obiekcie X.
- Wątek „B” trzyma obiekt X, ale czeka na obiekt Y.
- Wątek „C” trzyma obiekt Y, ale czeka na obiekt X.
To klasyczne zakleszczenie cykliczne (deadlock).
Jak używać tych narzędzi w praktyce?
- Uruchamiaj aplikację z flagą -XX:+FlightRecorder (albo po prostu użyj JDK 11+).
- Otwórz JMC, połącz się z procesem, zacznij nagrywanie (start recording).
- Analizuj „gorące miejsca”, długie blokady i konkurencję między wątkami.
3. Logowanie i śledzenie
W programach wielowątkowych debugowanie „na oko” prowadzi do bólu. Loguj wejścia/wyjścia z sekcji krytycznych (bloków synchronized), operacje na zmiennych współdzielonych, oczekiwania i wybudzenia wątków – dzięki temu zrozumiesz, kto i kiedy przejął lub zwolnił zasób.
Jak logować?
- Używaj standardowych narzędzi: java.util.logging, SLF4J, Log4j.
- Loguj nazwę wątku: Thread.currentThread().getName().
- Loguj czas i identyfikatory wątków.
- Loguj zdarzenia przejęcia/zwolnienia blokad.
Przykład logowania
synchronized(lock) {
System.out.println(Thread.currentThread().getName() + " przejął lock");
// sekcja krytyczna
System.out.println(Thread.currentThread().getName() + " wychodzi z lock");
}
Używanie nazw wątków
Nadawaj wątkom znaczące nazwy!
Thread t = new Thread(runnable, "MyWorker-1");
Przykład śledzenia za pomocą loggera
import java.util.logging.Logger;
public class Example {
private static final Logger logger = Logger.getLogger(Example.class.getName());
public void doWork() {
logger.info(Thread.currentThread().getName() + " rozpoczął pracę");
synchronized (this) {
logger.info(Thread.currentThread().getName() + " wszedł do synchronized");
// ...
}
logger.info(Thread.currentThread().getName() + " zakończył pracę");
}
}
4. Najlepsze praktyki diagnostyki
Minimalizuj zakres blokad
Trzymaj blokady możliwie jak najkrócej.
Zły przykład:
synchronized(lock) {
// długi I/O
// złożone obliczenia
// dostęp do bazy danych
// ... a dopiero potem praca na danych współdzielonych
}
Dobry przykład:
// poza synchronized: długi I/O, obliczenia
synchronized(lock) {
// tylko praca na danych współdzielonych
}
Używaj nazw wątków
Znaczące nazwy wątków oszczędzają czas przy analizie zrzutów i logów.
Pisz testy dla wielowątkowości
Używaj JUnit + CountDownLatch, aby modelować scenariusze współbieżne.
CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
// ...
latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // czekamy na zakończenie obu wątków
Używaj try-finally dla ReentrantLock
Lock lock = new ReentrantLock();
lock.lock();
try {
// sekcja krytyczna
} finally {
lock.unlock();
}
Dzięki temu nie zapomnisz zwolnić blokady nawet w przypadku wyjątku. Aby uniknąć zakleszczeń, używaj tryLock() z limitem czasu.
Dokumentuj, po co jest potrzebna synchronizacja
Komentarze w stylu „Tutaj potrzebny jest synchronized, ponieważ…” pomogą po czasie zrozumieć zamysł.
5. Praktyka: analiza zakleszczenia (deadlock) w programie testowym
Przykład kodu z zakleszczeniem
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1: przejął lockA");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockB) {
System.out.println("Thread-1: przejął lockB");
}
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2: przejął lockB");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
synchronized (lockA) {
System.out.println("Thread-2: przejął lockA");
}
}
}, "Thread-2");
t1.start();
t2.start();
}
}
Jak złapać deadlock
- Uruchom program – zawiesi się.
- Uzyskaj thread dump (jstack lub przez VisualVM).
- Znajdź „Thread-1” i „Thread-2” – zobaczysz, że każdy trzyma jeden lock i czeka na drugi.
- Na końcu zrzutu będzie sekcja „Found one Java-level deadlock”.
Jak usunąć
- Przejmuj blokady zawsze w tej samej kolejności.
- Używaj ReentrantLock z tryLock() i limitem czasu: jeśli nie udało się przejąć wszystkich blokad – zwolnij i spróbuj ponownie.
6. Typowe błędy przy diagnostyce programów wielowątkowych
Błąd nr 1: Nieumiejętność czytania thread dumpa. Początkujących programistów przeraża zrzut: „Co to za dziwne stack trace’y i statusy?” W praktyce wystarczy znać podstawowe stany i szukać BLOCKED/WAITING, aby uprościć analizę.
Błąd nr 2: Ignorowanie nazw wątków. Bez sensownych nazw rozumienie zrzutu to jak szukanie igły w stogu siana. Nie zaniedbuj nadawania nazw!
Błąd nr 3: Zbyt duże bloki synchronized. Jeśli synchronizujesz duże fragmenty kodu, wątki będą częściej się blokować jeden o drugi – widać to po częstych BLOCKED w zrzucie.
Błąd nr 4: Mylenie RUNNABLE z faktycznie pracującym wątkiem. RUNNABLE nie zawsze „biegnie” na procesorze. Planista JVM sam decyduje, kogo uruchamiać.
Błąd nr 5: Niekorzystanie z narzędzi monitorowania. Wielu nie zna VisualVM, JMC, Flight Recorder i męczy się z println. Używaj narzędzi – bardzo ułatwiają życie.
Błąd nr 6: Brak logowania operacji krytycznych. Bez logów niemal niemożliwe jest zrozumieć, kto kiedy przejął/zwolnił blokadę.
Błąd nr 7: Próby „na oko” łapania wyścigów danych. Wyścigi nie zawsze i nie od razu się ujawniają – używaj testów z CountDownLatch, prowokuj konkurencję przez Thread.yield() i analizuj stan zmiennych współdzielonych.
GO TO FULL VERSION