CodeGym /Kursy /JAVA 25 SELF /Diagnostyka i debugowanie programów wielowątkowych

Diagnostyka i debugowanie programów wielowątkowych

JAVA 25 SELF
Poziom 53, Lekcja 4
Dostępny

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

  1. Uruchom program – zawiesi się.
  2. Uzyskaj thread dump (jstack lub przez VisualVM).
  3. Znajdź „Thread-1” i „Thread-2” – zobaczysz, że każdy trzyma jeden lock i czeka na drugi.
  4. 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.

1
Zadanie
JAVA 25 SELF, poziom 53, lekcja 4
Niedostępne
Detektyw wewnątrz programu: Określenie stanu bieżącego wątku 🕵️‍♂️
Detektyw wewnątrz programu: Określenie stanu bieżącego wątku 🕵️‍♂️
1
Zadanie
JAVA 25 SELF, poziom 53, lekcja 4
Niedostępne
Dostęp do Sekretnego Pokoju: Logowanie wejścia i wyjścia z sekcji krytycznej 🚪
Dostęp do Sekretnego Pokoju: Logowanie wejścia i wyjścia z sekcji krytycznej 🚪
1
Ankieta/quiz
Problemy wielowątkowości, poziom 53, lekcja 4
Niedostępny
Problemy wielowątkowości
Problemy wielowątkowości
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION