CodeGym /Kurse /JAVA 25 SELF /Diagnose und Debugging von Multithread-Programmen

Diagnose und Debugging von Multithread-Programmen

JAVA 25 SELF
Level 53, Lektion 4
Verfügbar

1. Thread Dump und Analyse von Thread-Zuständen

Thread Dump (Abbild der Threads) – ist eine Momentaufnahme des Zustands aller Threads in der Anwendung zu einem bestimmten Zeitpunkt. Es ist wie ein Gruppenfoto all Ihrer Threads: wer womit beschäftigt ist, wer wo festhängt, wer auf wen wartet. Thread Dump – Ihr wichtigstes Werkzeug zur Suche nach Deadlock, Livelock und anderen mysteriösen Hängern.

Wie erhält man einen Thread Dump?

Über das Terminal (jstack):

Wenn Sie die PID des Java-Prozesses haben, führen Sie aus:

jstack <PID>

Der Befehl gibt in der Konsole den Zustand aller Threads aus, einschließlich ihres aktuellen Status und der gehaltenen Monitore (Locks).

Über die IDE (IntelliJ IDEA):
Im Menü „Run“ → „Show Running List“ → Prozess auswählen → „Thread Dump“.

Über VisualVM oder JConsole:
Öffnen Sie den Prozess, wechseln Sie auf den Reiter „Threads“ und erstellen Sie eine Aufnahme des Zustands.

Beispiel: Thread Dump

Ausschnitt aus dem Dump:

"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)

Hier sieht man, dass der Thread „Thread-1“ blockiert ist (BLOCKED), einen Monitor hält, aber auf einen anderen wartet. Wenn Sie mehrere solcher Threads sehen, die Ressource A halten und auf B warten, während ein anderer Thread B hält und auf A wartet – das ist ein klassischer Deadlock.

Thread-Zustände

Status Beschreibung
RUNNABLE Thread läuft oder ist bereit zu laufen
BLOCKED Wartet auf den Erwerb eines Monitors (Lock)
WAITING Wartet auf notify()/notifyAll() (z. B. durch wait() ausgelöst)
TIMED_WAITING Wartet mit Timeout (z. B. sleep, wait(timeout))
TERMINATED Thread ist beendet

Wichtig: Der Status RUNNABLE bedeutet nicht immer, dass der Thread gerade ausgeführt wird – er ist lediglich zur Ausführung bereit (der JVM-Scheduler startet ihn eventuell nicht sofort).

Woran erkennt man einen Deadlock?

Im Dump befinden sich mehrere Threads im Zustand BLOCKED; jeder wartet auf einen Monitor, der von einem anderen Thread aus demselben Satz gehalten wird.

Am Ende des Dumps schreibt jstack üblicherweise:

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"

Wenn Threads lange in BLOCKED oder WAITING hängen – ist das ein Grund für eine Untersuchung.

2. Überwachung und Profiling von Threads

VisualVM
VisualVM – ein kostenloses Tool, das in den meisten JDKs enthalten ist. Es ermöglicht, sich mit einem Prozess zu verbinden, den Zustand der Threads einzusehen, einen Thread Dump zu erstellen sowie CPU-Auslastung, aktive und „hängende“ Threads zu sehen.

Registerkarte Threads: Zeigt, wie viele Threads erstellt wurden, ihre Zustände und den Aktivitätsverlauf.

Thread Dump: Die Schaltfläche „Thread Dump“ erstellt eine Aufnahme, ähnlich wie jstack.

Java Mission Control und Flight Recorder

Java Mission Control (JMC): Ein fortgeschrittenes Werkzeug zur Analyse der JVM in Echtzeit. Hilft, Sperren, Laufzeiten, Allokationen und Latenzen zu untersuchen.

Java Flight Recorder (JFR): Ein in die JVM integrierter Profiler, der Ereignisse zu Threads, Sperren, Pausen usw. sammelt.

Beispiel: Überwachung von Sperren

In VisualVM oder JMC kann man sehen, dass:

  • Thread „A“ ist auf Objekt X blockiert.
  • Thread „B“ hält Objekt X, wartet aber auf Objekt Y.
  • Thread „C“ hält Objekt Y, wartet aber auf Objekt X.

Das ist eine klassische zyklische Blockierung (deadlock).

Wie setzt man diese Werkzeuge in der Praxis ein?

  • Starten Sie die Anwendung mit dem Schalter -XX:+FlightRecorder (oder verwenden Sie einfach JDK 11+).
  • Öffnen Sie JMC, verbinden Sie sich mit dem Prozess und starten Sie eine Aufzeichnung (start recording).
  • Analysieren Sie „Hotspots“, lange Sperren und die Konkurrenz zwischen Threads.

3. Logging und Tracing

In Multithread-Programmen führt Debugging „nach Gefühl“ zu Schmerzen. Loggen Sie Ein- und Austritt in kritische Abschnitte (synchronized-Blöcke), Operationen mit gemeinsamen Variablen sowie das Warten und Aufwecken von Threads – so verstehen Sie, wer wann eine Ressource ergriffen oder freigegeben hat.

Wie loggt man?

  • Verwenden Sie Standardwerkzeuge: java.util.logging, SLF4J, Log4j.
  • Loggen Sie den Thread-Namen: Thread.currentThread().getName().
  • Loggen Sie Zeitstempel und Thread-IDs.
  • Loggen Sie Ereignisse zum Erwerb/Freigeben von Sperren.

Beispiel für Logging

synchronized(lock) {
    System.out.println(Thread.currentThread().getName() + " hat lock erworben");
    // kritischer Abschnitt
    System.out.println(Thread.currentThread().getName() + " verlässt lock");
}

Verwendung von Thread-Namen

Geben Sie Threads aussagekräftige Namen!

Thread t = new Thread(runnable, "MyWorker-1");

Beispiel für Tracing mit einem Logger

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() + " hat die Arbeit begonnen");
        synchronized (this) {
            logger.info(Thread.currentThread().getName() + " ist in synchronized eingetreten");
            // ...
        }
        logger.info(Thread.currentThread().getName() + " hat die Arbeit beendet");
    }
}

4. Best Practices für die Diagnose

Minimieren Sie den Sperrbereich

Halten Sie Sperren so kurz wie möglich.

Schlechtes Beispiel:

synchronized(lock) {
    // langwierige Ein-/Ausgabe
    // aufwendige Berechnungen
    // Zugriff auf die Datenbank
    // ... und erst danach Arbeit mit gemeinsamen Daten
}

Gutes Beispiel:

// außerhalb von synchronized: langwierige Ein-/Ausgabe, Berechnungen

synchronized(lock) {
    // nur Arbeit mit gemeinsamen Daten
}

Verwenden Sie Thread-Namen

Aussagekräftige Thread-Namen sparen Zeit bei der Analyse von Dumps und Logs.

Schreiben Sie Tests für Multithreading

Verwenden Sie JUnit + CountDownLatch, um konkurrierende Szenarien zu modellieren.

CountDownLatch latch = new CountDownLatch(2);
Runnable task = () -> {
    // ...
    latch.countDown();
};
new Thread(task, "Worker-1").start();
new Thread(task, "Worker-2").start();
latch.await(); // wir warten auf den Abschluss beider Threads

Verwenden Sie try-finally für ReentrantLock

Lock lock = new ReentrantLock();
lock.lock();
try {
    // kritischer Abschnitt
} finally {
    lock.unlock();
}

So vergessen Sie die Sperre auch bei einer Exception nicht freizugeben. Um Deadlocks zu vermeiden, verwenden Sie tryLock() mit Timeout.

Dokumentieren Sie, warum Synchronisierung nötig ist

Kommentare „Hier ist synchronized nötig, weil …“ helfen später, die Absicht zu verstehen.

5. Praxis: Analyse eines Deadlocks in einem Testprogramm

Beispielcode mit Deadlock

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: hat lockA erworben");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                synchronized (lockB) {
                    System.out.println("Thread-1: hat lockB erworben");
                }
            }
        }, "Thread-1");

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("Thread-2: hat lockB erworben");
                try { Thread.sleep(100); } catch (InterruptedException ignored) {}
                synchronized (lockA) {
                    System.out.println("Thread-2: hat lockA erworben");
                }
            }
        }, "Thread-2");

        t1.start();
        t2.start();
    }
}

Wie findet man den Deadlock

  1. Starten Sie das Programm – es wird hängen bleiben.
  2. Erstellen Sie einen Thread Dump (jstack oder über VisualVM).
  3. Suchen Sie „Thread-1“ und „Thread-2“ – Sie sehen, dass jeder einen Lock hält und auf den anderen wartet.
  4. Am Ende des Dumps gibt es den Abschnitt „Found one Java-level deadlock“.

Wie beheben

  • Erwerben Sie Sperren immer in derselben Reihenfolge.
  • Verwenden Sie ReentrantLock mit tryLock() und Timeout: Wenn nicht alle Locks erworben werden können – geben Sie frei und versuchen Sie es erneut.

6. Typische Fehler bei der Diagnose von Multithread-Programmen

Fehler Nr. 1: Unfähigkeit, einen thread dump zu lesen. Einsteiger lassen sich vom Dump einschüchtern: „Was sind das für seltsame Stacktraces und Statuswerte?“ In der Praxis genügt es, die wichtigsten Zustände zu kennen und nach BLOCKED/WAITING zu suchen, um die Analyse zu vereinfachen.

Fehler Nr. 2: Ignorieren der Thread-Namen. Ohne aussagekräftige Namen ist das Lesen des Dumps wie die Suche nach der Nadel im Heuhaufen. Nehmen Sie sich die Zeit, Namen zu vergeben!

Fehler Nr. 3: Zu große synchronized-Blöcke. Wenn Sie große Codebereiche synchronisieren, blockieren sich Threads häufiger gegenseitig – das sieht man an häufigen BLOCKED-Zuständen im Dump.

Fehler Nr. 4: Verwechslung von RUNNABLE mit einem tatsächlich laufenden Thread. RUNNABLE läuft nicht zwangsläufig auf der CPU. Der JVM-Scheduler entscheidet selbst, wen er startet.

Fehler Nr. 5: Nichtnutzung von Monitoring-Werkzeugen. Viele kennen VisualVM, JMC, Flight Recorder nicht und quälen sich mit println. Nutzen Sie die Werkzeuge – sie vereinfachen das Leben erheblich.

Fehler Nr. 6: Kein Logging kritischer Operationen. Ohne Logs ist es nahezu unmöglich zu verstehen, wer wann eine Sperre erworben/freigegeben hat.

Fehler Nr. 7: Versuche, Datenrennen „mit bloßem Auge“ zu finden. Race Conditions treten nicht immer und nicht sofort auf – verwenden Sie Tests mit CountDownLatch, provozieren Sie Konkurrenz über Thread.yield() und analysieren Sie den Zustand gemeinsamer Variablen.

1
Aufgabe
JAVA 25 SELF, Level 53, Lektion 4
Gesperrt
Detektiv im Programm: Zustand des aktuellen Threads ermitteln 🕵️‍♂️
Detektiv im Programm: Zustand des aktuellen Threads ermitteln 🕵️‍♂️
1
Aufgabe
JAVA 25 SELF, Level 53, Lektion 4
Gesperrt
Zugang zum geheimen Raum: Protokollierung von Betreten und Verlassens des kritischen Abschnitts 🚪
Zugang zum geheimen Raum: Protokollierung von Betreten und Verlassens des kritischen Abschnitts 🚪
1
Umfrage/Quiz
Probleme der Nebenläufigkeit, Level 53, Lektion 4
Nicht verfügbar
Probleme der Nebenläufigkeit
Probleme der Nebenläufigkeit
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION