1. Tiefer einsteigen in Deadlocks
Ein Deadlock („gegenseitige Blockierung“) ist eine Situation, in der zwei oder mehr Threads einander unbegrenzt lange warten lassen: Jeder hält eine Ressource und versucht, eine weitere zu bekommen, die bereits von einem anderen Thread gehalten wird. Am Ende kann niemand fortfahren und das Programm hängt sich auf. Es ist wie zwei Autos auf einer schmalen Brücke, Stoßstange an Stoßstange: Solange keines zurücksetzt, fährt niemand weiter.
In Java ist ein Deadlock kein Ausnahmefehler (Exception), sondern ein „Einfrieren“ des Programms. Threads beenden sich nicht und stürzen nicht mit einem Fehler ab, sondern warten einfach endlos aufeinander. Daher sind solche Fehler tückisch: Sie zeigen sich nur „unter bestimmten Umständen“.
Vier Bedingungen für das Entstehen eines Deadlocks
- 1. Gegenseitiger Ausschluss (Mutual Exclusion)
Eine Ressource kann jeweils nur von einem Thread gehalten werden (z. B. ein Objektmonitor, eine Datei). - 2. Halten und Warten (Hold and Wait)
Ein Thread hält eine Ressource und versucht, eine zweite zu bekommen, ohne die erste freizugeben. - 3. Keine Zwangsentziehung (No Preemption)
Eine Ressource kann einem Thread nicht „weggenommen“ werden: Nur der Thread selbst kann sie freigeben. - 4. Zyklisches Warten (Circular Wait)
Es gibt eine geschlossene Kette, in der jeder Thread auf eine Ressource wartet, die vom nächsten in der Kette gehalten wird.
Ein Deadlock ist nur möglich, wenn ALLE diese Bedingungen gleichzeitig erfüllt sind. Es reicht, mindestens eine zu verletzen – dann ist eine gegenseitige Blockierung unmöglich.
2. Codebeispiel mit Deadlock
Schema
- Es gibt zwei Ressourcen: lock1 und lock2 (gewöhnliche Objekte).
- Thread A erfasst zuerst lock1 und versucht dann, lock2 zu bekommen.
- Thread B erfasst zuerst lock2 und versucht dann, lock1 zu bekommen.
Beispiel (zu Hause bitte nicht nachmachen!)
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Thread 1
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: hat lock1 gesperrt");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 1: versucht, lock2 zu sperren");
synchronized (lock2) {
System.out.println("Thread 1: hat lock2 gesperrt");
}
}
});
// Thread 2
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: hat lock2 gesperrt");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 2: versucht, lock1 zu sperren");
synchronized (lock1) {
System.out.println("Thread 2: hat lock1 gesperrt");
}
}
});
thread1.start();
thread2.start();
}
}
Was passiert beim Start?
- Thread 1 erfasst lock1, Thread 2 — lock2.
- Beide „schlafen“ für 100 Millisekunden und schaffen es, die erste Sperre zu setzen.
- Dann versucht jeder, die zweite Sperre zu setzen, die bereits vom anderen Thread gehalten wird.
- Beide Threads warten endlos aufeinander — es entsteht ein Deadlock.
In der Konsole sehen Sie:
Thread 1: hat lock1 gesperrt
Thread 2: hat lock2 gesperrt
Thread 1: versucht, lock2 zu sperren
Thread 2: versucht, lock1 zu sperren
Warum entsteht ein Deadlock? Ausführliche Analyse
- Gegenseitiger Ausschluss: Jede Sperre kann jeweils nur von einem Thread gehalten werden.
- Halten und Warten: Thread 1 hält lock1 und wartet auf lock2; Thread 2 hält lock2 und wartet auf lock1.
- Keine Zwangsentziehung: Niemand kann die Sperre von außen „herausziehen“ — nur durch das Verlassen des synchronized-Blocks.
- Zyklisches Warten: Das Warten ist zwischen zwei Threads im Kreis geschlossen.
3. Deadlocks vermeiden und verhindern
Ressourcen in derselben Reihenfolge sperren
Die wichtigste Regel: Wenn mehrere Threads mehrere Ressourcen sperren müssen, tun Sie dies immer in derselben Reihenfolge. Zum Beispiel: zuerst lock1, dann lock2 — und zwar an allen Stellen im Code.
public void doSomething() {
Object firstLock = lock1;
Object secondLock = lock2; // einheitliche Reihenfolge "lock1 -> lock2"
synchronized (firstLock) {
synchronized (secondLock) {
// Arbeit mit beiden Ressourcen
}
}
}
Bei einheitlicher Reihenfolge ist zyklisches Warten unmöglich — die Deadlock-Bedingung wird verletzt.
tryLock mit Timeout verwenden (ReentrantLock)
Wenn im Voraus nicht klar ist, welche Ressourcen benötigt werden, verwenden Sie ReentrantLock und die Methode tryLock, um nicht endlos zu warten.
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public void doWork() {
try {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Kritischer Abschnitt
} finally {
lock2.unlock();
}
} else {
System.out.println("Konnte lock2 nicht sperren, breche ab");
}
} finally {
lock1.unlock();
}
} else {
System.out.println("Konnte lock1 nicht sperren, breche ab");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Plus: Man kann den Vorgang abbrechen und später erneut versuchen. Minus: Der Code ist komplexer, aber Deadlocks sind ausgeschlossen.
Halten Sie Sperren möglichst kurz
Halten Sie eine Sperre so kurz wie möglich. Führen Sie innerhalb von synchronized/lock nur das Nötigste aus; alles Andere gehört außerhalb der kritischen Sektion.
Verschachtelte Sperren vermeiden
Je weniger verschachtelte synchronized/lock-Blöcke, desto geringer das Deadlock-Risiko. Wenn möglich, verwenden Sie eine einzige Sperre für zusammengehörige Ressourcen.
Verwenden Sie fertige thread-sichere Strukturen
Die Standardbibliothek löst bereits viele Aufgaben: Collections wie ConcurrentHashMap und andere Klassen aus java.util.concurrent minimieren das Deadlock-Risiko und vereinfachen den Code.
4. Deadlock-Diagnose
Thread Dump und jstack
Thread Dump — eine Momentaufnahme des Zustands aller Threads in der JVM.
So erhalten Sie ihn in der Konsole:
jstack <pid>
Dabei ist <pid> die Kennung des Java-Prozesses (ermittelbar über jps).
In IDEs gibt es gewöhnlich eine „Thread Dump“-Schaltfläche in der Debug-Ansicht.
Suchen Sie im Dump nach:
- Statuswerten BLOCKED oder WAITING.
- Meldungen wie waiting to lock ... und locked ....
- Der JVM-Meldung: "Found one Java-level deadlock:" — wenn eine gegenseitige Blockierung erkannt wurde.
Beispielausschnitt eines Dumps:
"Thread-1":
waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
which is held by "Thread-1"
Das ist ein Deadlock: Die Threads „halten“ einander fest.
VisualVM und weitere Werkzeuge
- VisualVM — ein kostenloses Werkzeug zur Thread-Analyse und Deadlock-Erkennung.
- Java Mission Control / Flight Recorder — fortgeschrittenes Monitoring und Profiling.
In VisualVM können Sie den Thread-Baum, deren Zustände ansehen und eine Benachrichtigung über eine gegenseitige Blockierung erhalten.
Tabelle: „Wie vermeidet man Deadlocks“
| Ursache für Deadlock | Wie vermeiden |
|---|---|
| Ressourcen in unterschiedlicher Reihenfolge sperren | Immer dieselbe Reihenfolge der Ressourcensperren einhalten |
| Verschachteltes synchronized | Verschachtelung minimieren, nach Möglichkeit eine einzige Sperre verwenden |
| Langes Halten einer Sperre | Arbeit innerhalb der kritischen Sektion reduzieren |
| Gleichzeitige Verwendung mehrerer Sperren | tryLock mit Timeout verwenden und bei Bedarf zurücksetzen |
| Nicht thread-sichere Collections | Konkurrierende Collections verwenden (ConcurrentHashMap u. a.) |
5. Typische Fehler im Umgang mit Deadlocks
Fehler Nr. 1: Ressourcen in unterschiedlicher Reihenfolge sperren. Der häufigste Grund — verschiedene Threads nehmen Sperren in unterschiedlicher Reihenfolge. Selbst wenn es „schneller“ scheint, halten Sie sich an eine einheitliche Reihenfolge — sonst ist zyklisches Warten garantiert.
Fehler Nr. 2: Verschachtelte synchronized-Blöcke ohne Notwendigkeit. Überflüssige Verschachtelung erhöht das Deadlock-Risiko und verkompliziert den Code. Vereinfachen Sie das Sperrenmodell.
Fehler Nr. 3: tryLock und Timeouts ignorieren. Eine Sperre per synchronized lässt den Thread endlos warten. Wenn Sie nicht sicher sind, verwenden Sie tryLock mit Timeout und eine Abbruch-/Rollback-Logik.
Fehler Nr. 4: Lange Ausführung innerhalb einer Sperre. Netzwerkaufrufe, I/O und rechenintensive Operationen unter einer Sperre erhöhen das Risiko deutlich. Verlegen Sie sie außerhalb der kritischen Sektion.
Fehler Nr. 5: Thread Dump nicht analysieren. Der Thread Dump ist Ihr bester Freund bei der Suche nach gegenseitigen Blockierungen. Verwenden Sie jstack und analysieren Sie die Zustände BLOCKED/WAITING, die Ketten „holding/waiting to lock“.
GO TO FULL VERSION