1. Grundlegende Thread-Zustände in Java
In Java ist ein Thread nicht einfach nur „gestartet“ oder „angehalten“. Er hat einen ganzen Lebenszyklus, und in jeder Phase verhält sich der Thread unterschiedlich. Das Verständnis dieser Phasen ist der Schlüssel zum Schreiben stabiler Multithread-Anwendungen und zur Fehlersuche bei seltsamen „Hängern“ und „unerwarteten Beendigungen“.
Welche Zustände gibt es?
Java definiert die folgenden grundlegenden Thread-Zustände (sie sind im Enum Thread.State aufgelistet):
| Zustand | Beschreibung |
|---|---|
|
Der Thread wurde erstellt, aber noch nicht gestartet (new Thread(...), aber start() wurde nicht aufgerufen) |
|
Der Thread ist bereit zur Ausführung oder wird gerade ausgeführt |
|
Der Thread wartet auf die Freigabe des Monitors (blockiert an einem synchronized-Block) |
|
Der Thread wartet darauf, dass ein anderer Thread ihn „aufweckt“ (z. B. über Object.wait()) |
|
Der Thread wartet mit Timeout (z. B. Thread.sleep(1000), wait(1000), join(1000)) |
|
Der Thread hat die Ausführung beendet |
Interessante Tatsache:
In älteren Büchern und Artikeln können Ihnen andere Bezeichnungen oder leicht andere Schemata begegnen. Seit Java 5+ gelten diese Zustände jedoch als Standard.
Visuelles Schema des Thread-Lebenszyklus
stateDiagram-v2
[*] --> NEW
NEW --> RUNNABLE: start()
RUNNABLE --> BLOCKED: Versuch, in synchronized einzutreten, aber der Monitor ist belegt
BLOCKED --> RUNNABLE: Monitor freigegeben
RUNNABLE --> WAITING: wait(), join()
RUNNABLE --> TIMED_WAITING: sleep(), wait(timeout), join(timeout)
WAITING --> RUNNABLE: notify()/notifyAll(), join() ist beendet
TIMED_WAITING --> RUNNABLE: Timeout abgelaufen / notify()/notifyAll()
RUNNABLE --> TERMINATED: run() ist beendet
WAITING --> TERMINATED: run() ist beendet (selten)
TIMED_WAITING --> TERMINATED: run() ist beendet (selten)
2. Methoden zur Thread-Steuerung
Schlaf: Thread.sleep(long millis)
Manchmal muss ein Thread „schlafen“, um anderen nicht im Weg zu stehen oder auf ein Ereignis zu warten. Die Methode Thread.sleep(ms) versetzt den Thread für die angegebene Anzahl von Millisekunden in den Zustand
TIMED_WAITING.
System.out.println("Der Thread schläft für 2 Sekunden...");
Thread.sleep(2000); // Schlafen 2 Sekunden
System.out.println("Der Thread ist aufgewacht!");
- Nach dem Ende des Schlafs kehrt der Thread in den Zustand
zurück (arbeitsbereit).RUNNABLE - Wenn der Thread während des Schlafs unterbrochen wird, wird eine InterruptedException ausgelöst.
Auf das Ende eines anderen Threads warten: join()
Manchmal muss man nicht nur einen Thread starten, sondern auch warten, bis er seine Arbeit beendet. Dafür gibt es in Java die Methode join():
Thread t = new Thread(() -> {
System.out.println("Arbeite...");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("Fertig!");
});
t.start();
System.out.println("Warte auf die Beendigung des Threads t...");
t.join(); // Der aktuelle Thread (z. B. main) wartet auf t
System.out.println("Thread t ist beendet!");
Hier beginnt der Haupt-Thread zu warten, sobald join() aufgerufen wird. Die ganze Zeit befindet er sich im Zustand
WAITING, bis der Thread
t seine Ausführung beendet. Es gibt auch eine Variante mit Timeout –
join(long millis). In diesem Fall wartet der Thread nur eine begrenzte Zeit, und sein Zustand ist
TIMED_WAITING.
Thread-Unterbrechung: interrupt()
Es gibt Situationen, in denen man einen Thread höflich bitten muss, seine Arbeit zu beenden, etwa wenn der Benutzer auf „Abbrechen“ klickt. Dafür wird die Methode interrupt() verwendet:
Thread t = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// Wir arbeiten ...
}
System.out.println("Thread durch Unterbrechung beendet!");
});
t.start();
// ... nach einiger Zeit:
t.interrupt(); // Signal an den Thread: "Zeit anzuhalten"
Wichtig: Der Aufruf von interrupt() beendet den Thread nicht sofort. Er setzt lediglich ein spezielles Flag. Der Thread selbst sollte dieses Flag regelmäßig über isInterrupted() prüfen und sich selbst sauber beenden. Befindet sich der Thread in dieser Zeit im Schlaf (sleep()) oder im Warten (wait()), sieht er nicht nur das Flag, sondern erhält sofort die Ausnahme InterruptedException. Genau so sieht in Java der „korrekte“ Weg aus, Threads zu stoppen: Das Programm unterbricht sie nicht mit Gewalt, sondern teilt mit, dass es Zeit ist, zu enden.
3. Beispiele für Übergänge zwischen Zuständen
Schauen wir uns anhand von Beispielen an, wie ein Thread durch seine Zustände „reist“.
Beispiel 1: NEW
→ RUNNABLE
→ TERMINATED
NEWRUNNABLETERMINATEDThread t = new Thread(() -> System.out.println("Hallo!"));
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (oder TERMINATED, wenn der Thread sehr schnell ist)
t.join();
System.out.println(t.getState()); // TERMINATED
Beispiel 2: RUNNABLE
→ TIMED_WAITING
→ RUNNABLE
→ TERMINATED
RUNNABLETIMED_WAITINGRUNNABLETERMINATEDThread t = new Thread(() -> {
try {
System.out.println("Schlafe ein...");
Thread.sleep(1000); // TIMED_WAITING
System.out.println("Aufgewacht!");
} catch (InterruptedException e) {
System.out.println("Thread wurde unterbrochen!");
}
});
t.start();
Beispiel 3: RUNNABLE
→ WAITING
mit join()
RUNNABLEWAITINGThread t1 = new Thread(() -> {
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
System.out.println("t1 ist beendet");
});
Thread t2 = new Thread(() -> {
try {
t1.join(); // t2 wartet auf t1, befindet sich in WAITING
System.out.println("t2 hat auf t1 gewartet");
} catch (InterruptedException ignored) {}
});
t1.start();
t2.start();
Beispiel 4: BLOCKED
BLOCKEDObject lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
System.out.println("t1 hat den synchronized-Block verlassen");
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("t2 ist in den synchronized-Block eingetreten");
}
});
t1.start();
Thread.sleep(100); // Geben wir t1 Zeit, lock zu übernehmen
t2.start();
Thread.sleep(100); // Geben wir t2 die Möglichkeit, in synchronized zu gelangen
System.out.println("Zustand von t2: " + t2.getState()); // BLOCKED
4. Wie ermittelt man den Zustand eines Threads? Die Methoden isAlive() und getState()
- isAlive() – gibt true zurück, wenn der Thread gestartet ist und noch nicht beendet wurde (
ergibt bereits false).TERMINATED - getState() – gibt den aktuellen Zustand des Threads zurück (einen Wert aus dem Enum Thread.State).
Thread t = new Thread(() -> {});
System.out.println(t.isAlive()); // false (NEW)
t.start();
System.out.println(t.isAlive()); // true (RUNNABLE/WAITING/...)
t.join();
System.out.println(t.isAlive()); // false (TERMINATED)
5. Warum man einen Thread nicht direkt „killen“ kann und weitere Praxistipps
Keine Methode „kill“!
In Java gibt es keine Methode, mit der man einen Thread „auf Kommando“ töten kann. Warum? Weil das unsicher ist: Hält der Thread gerade eine Ressource (Datei, Verbindung, Sperre), kann seine erzwungene Beendigung das System in einen inkonsistenten Zustand versetzen.
Veraltete Methoden: stop(), suspend(), resume()
In sehr alten Java-Versionen gab es die Methoden stop(), suspend(), resume(). Heute sind sie als @Deprecated markiert, und ihre Verwendung wird dringend abgeraten. Warum?
- stop() kann den Thread jederzeit beenden und Daten in einem inkonsistenten Zustand zurücklassen.
- suspend() kann einen Thread „einfrieren“, der eine Sperre hält – dann hängt das gesamte Programm.
- resume() kann einen Thread manchmal nicht „auftauen“, wenn dieser bereits beendet ist.
Moderner Ansatz:
Der Thread sollte sich sauber selbst beenden, indem er auf das Unterbrechungs-Flag reagiert (isInterrupted()) oder auf andere Signale.
Best Practices
- Rufen Sie keine Methoden auf, die als veraltet markiert sind.
- Verwenden Sie das Unterbrechungs-Flag zum Stoppen des Threads (interrupt() und die Prüfung von isInterrupted()).
- Beobachten Sie beim Debuggen die Thread-Zustände – das hilft, Hänger und Deadlocks zu finden.
- Vergessen Sie join() nicht, wenn Sie auf die Beendigung eines Threads warten müssen.
6. Typische Fehler im Umgang mit dem Thread-Lebenszyklus
Fehler Nr. 1: Einen Thread erneut starten.
In Java kann ein Thread nur einmal gestartet werden. Wird start() ein zweites Mal aufgerufen, erhalten Sie eine IllegalThreadStateException. Wenn Sie die Aufgabe wiederholen müssen, erstellen Sie ein neues Thread-Objekt.
Thread t = new Thread(() -> {});
t.start();
t.start(); // Wirft eine Ausnahme!
Fehler Nr. 2: run() und start() verwechseln.
Ein direkter Aufruf von run() startet den Code nicht in einem neuen Thread – er wird im aktuellen Thread ausgeführt (z. B. in main). Nur start() startet tatsächlich einen neuen Thread.
Fehler Nr. 3: InterruptedException nicht behandeln.
Wenn ein Thread schläft oder wartet und unterbrochen wird, wird eine InterruptedException ausgelöst. Ignoriert man sie, kann der Thread „für immer einschlafen“ oder unerwartet enden.
Fehler Nr. 4: Den Zustand des Threads nicht prüfen.
Manchmal hängt ein Programm, weil ein Thread auf einen anderen wartet, der bereits beendet ist oder niemals starten wird. Verwenden Sie getState() und isAlive() zur Diagnose.
Fehler Nr. 5: Veraltete Thread-Steuerungsmethoden verwenden.
Die Methoden stop(), suspend(), resume() sind schlecht. Verwenden Sie sie nicht – auch dann nicht, wenn Sie „schnell alles reparieren“ möchten.
GO TO FULL VERSION