Deadlock (gegenseitige Blockierung) ist ein Zustand, in dem zwei oder mehr Threads endlos auf die Freigabe von Ressourcen warten, die von anderen Teilnehmern gehalten werden. Laut Oracle Java Tutorials (2024) entsteht Deadlock bei zirkulärem Warten, wenn jeder Thread eine Sperre hält, die ein anderer Thread benötigt. Ohne spezielle Erkennungswerkzeuge stoppt Deadlock die Ausführung der Anwendung vollständig ohne sichtbare Fehler.
Wichtige Punkte
Deadlock ist eine Situation in der Multithread-Programmierung, in der zwei oder mehr Threads einander dauerhaft blockieren. Jeder Thread hält eine Ressource, die ein anderer Thread benötigt, und gibt sie nicht frei, während er auf die fehlende Ressource wartet. Infolgedessen kann keiner der Threads die Ausführung fortsetzen.
In der mobilen Entwicklung ist Deadlock besonders kritisch, da es keine Ausnahmen oder Abstürze verursacht. Die Anwendung reagiert einfach nicht mehr auf Benutzeraktionen (ANR — Application Not Responding), und der einzige Ausweg ist die erzwungene Beendigung des Prozesses. Laut Google (Android Performance Patterns, 2023) stehen etwa 15% der ANR-Berichte in der Google Play Console im Zusammenhang mit gegenseitigen Blockierungen in Hintergrund-Threads.
Der Hauptunterschied zwischen Deadlock und anderen Parallelitätsproblemen ist seine Irreversibilität ohne äußeren Eingriff. Threads geben Ressourcen nicht von selbst frei, da der Betriebssystem-Scheduler eine Sperre nicht zwangsweise entziehen kann. Dies unterscheidet Deadlock von Livelock, bei dem Threads zwar aktiv sind, aber keine nützliche Arbeit leisten.
1971 formulierte Edward G. Coffman vier zwingende Bedingungen, die für das Auftreten von Deadlock notwendig sind. Fehlt mindestens eine davon, ist eine gegenseitige Blockierung unmöglich. Diese Bedingungen sind als Coffman-Bedingungen bekannt und bilden die Grundlage aller Deadlock-Präventionsalgorithmen.
Eine Ressource kann zu jedem Zeitpunkt von nur einem Thread erfasst werden. Wenn eine Ressource das gleichzeitige Lesen durch mehrere Threads erlaubt (z. B. ReadWriteLock im Lesemodus), tritt Deadlock nicht auf. Diese Bedingung ergibt sich aus der Natur von Mutex und Sperren.
Ein Thread hält eine bereits erworbene Ressource und wartet gleichzeitig auf den Erwerb einer weiteren Ressource. Wenn ein Thread die aktuelle Ressource freigeben kann, bevor er die nächste anfordert (durch Zwei-Phasen-Sperrung), wird die Hold-and-Wait-Bedingung gebrochen. In Android zeigt sich dies oft, wenn ein Thread eine Datenbanksperre hält und versucht, eine SharedPreferences-Sperre zu erwerben.
Das Betriebssystem kann einem Thread nicht zwangsweise eine Sperre entziehen. Die Ressource wird nur freigegeben, wenn der Thread sie selbst freigibt. In einigen Systemen (z. B. SQLite WAL-Modus) wird ein erzwungener Entzug auf der Ebene einzelner Operationen implementiert, was das Deadlock-Risiko verringert.
Es existiert eine geschlossene Kette von Threads, von denen jeder auf eine Ressource wartet, die vom nächsten in der Kette gehalten wird. Zum Beispiel hält Thread A Ressource 1 und wartet auf Ressource 2, Thread B hält Ressource 2 und wartet auf Ressource 1. Dies ist die einzige Bedingung, die ein Entwickler architektonisch beseitigen kann — durch eine Sperrhierarchie. Wenn alle Threads Ressourcen in einer streng definierten globalen Reihenfolge erwerben, ist ein Zyklus physisch unmöglich.
In der Praxis tritt Deadlock in Android-Anwendungen am häufigsten aufgrund der impliziten Überschneidung von Sperren verschiedener Ebenen auf: Datenbanksperre (Room), SharedPreferences-Sperre und In-Memory-Collection-Sperre. Jede dieser Sperren wird von verschiedenen Komponenten verwaltet, und ohne ein zentralisiertes Protokoll für die Erfassungsreihenfolge erzeugen Entwickler unbeabsichtigt Zyklen.
Betrachten wir ein klassisches Beispiel für gegenseitige Blockierung — zwei Threads erwerben Sperren in unterschiedlicher Reihenfolge. Wenn der erste Thread Ressource A sperrt und B zu erwerben versucht, und der zweite B sperrt und A zu erwerben versucht, tritt Deadlock auf.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // Arbeitssimulation
synchronized(lockB) {
println("operationA abgeschlossen")
}
}
}
fun operationB() {
synchronized(lockB) { // umgekehrte Reihenfolge
Thread.sleep(50)
synchronized(lockA) {
println("operationB abgeschlossen")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Anwendung wird für immer hängen — Deadlock!
}
In diesem Beispiel erwirbt operationA lockA und operationB erwirbt lockB. Dann versucht jeder, die zweite Sperre zu erwerben — und beide warten endlos. Das Programm hängt ohne Fehlermeldung. Die einzige Möglichkeit zur Behebung besteht darin, dieselbe Reihenfolge der Sperrerfassung in allen Methoden zu gewährleisten.
Diese drei Parallelitätsprobleme werden oft verwechselt, aber ihre Mechanismen und Folgen sind grundlegend unterschiedlich. Deadlock — vollständiger Stillstand, Starvation — unendliches Warten auf eine Ressource, Livelock — aktive Untätigkeit. Das Verständnis der Unterschiede ist für die Wahl der richtigen Lösungsstrategie von entscheidender Bedeutung.
| Merkmal | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Thread-Zustand | Blockiert (BLOCKED) | Bereit (RUNNABLE) | Aktiv (RUNNABLE) |
| Arbeitsausführung | Nein | Nein | Ja, aber nutzlos |
| Ursache | Zirkuläres Warten | Ungerechte Terminierung | Falsche Konfliktbehandlung |
| Erkennung | Thread Dump, Timeouts | Fortschrittsüberwachung | Wiederholungszähler |
Starvation (Aushungerung) tritt auf, wenn der Scheduler die Ausführung eines Threads mit niedriger Priorität ständig zugunsten anderer verschiebt. Im Gegensatz zu Deadlock ist der Thread nicht blockiert — er ist ausführungsbereit, erhält aber keine CPU-Zeit. In Android ist ein typisches Szenario ein Hintergrund-Thread mit niedriger Priorität, der nie ausgeführt wird, wenn der UI-Thread und Service-Threads ständig aktiv sind.
Livelock (aktive Sperrung) ist eine Situation, in der Threads nicht blockiert sind, aber endlos auf die Aktionen des anderen reagieren, ohne nützliche Arbeit zu leisten. Die klassische Analogie — zwei Personen treffen sich in einem Korridor und beide versuchen auszuweichen, indem sie sich in dieselbe Richtung bewegen. Im Gegensatz zu Deadlock verbrauchen Threads in Livelock CPU und entladen den Akku des Geräts.
Thread Dump ist das primäre Werkzeug zur Erkennung gegenseitiger Blockierungen in JVM und Android Runtime. Während eines Dumps analysiert die JVM automatisch den Abhängigkeitsgraphen zwischen Monitoren und markiert Deadlock-Zyklen. In Android Studio können Thread-Dumps über Android Profiler oder den Befehl kill -3 PID von der ADB Shell aus abgerufen werden.
Die automatische Deadlock-Erkennung zur Laufzeit wird durch Watchdog-Timer implementiert. Wenn ein Thread einen Vorgang nicht innerhalb eines bestimmten Timeouts abschließt, initiiert der Watchdog einen Dump und sendet einen Bericht an das Crash-Reporting-System (Firebase Crashlytics, Sentry). Laut Sentry (Issue Resolution Report, 2024) verkürzt die Konfiguration eines Watchdogs die Deadlock-Diagnosezeit von Wochen auf wenige Stunden.
Während der Entwicklung sind der statische Analyzer ThreadSafe von JetBrains und das Checker Framework mit dem Lock Checker-Modul effektiv. Diese Werkzeuge analysieren die Reihenfolge der Sperrerfassung auf Quellcode-Ebene und warnen vor potenziellen Zyklen. Zusätzlich wird Test-Driven Deadlock Detection empfohlen — Stresstests, die Operationen mit verschiedenen Sperrreihenfolgen in Hunderten von Threads ausführen.
Besondere Beachtung verdient die Cooperative Deadlock Detection — eine Methode, bei der Threads Informationen über erworbene Sperren über eine globale Registrierung austauschen. Wenn ein Thread einen potenziellen Zyklus erkennt, gibt er alle Ressourcen frei und wiederholt den Vorgang. Dieser Ansatz wird in verteilten Systemen (Apache ZooKeeper, Google Chubby) verwendet und findet allmählich Einzug in die mobile Entwicklung durch Bibliotheken wie Jetpack Sync.
Der zuverlässigste Weg ist die Festlegung einer globalen Reihenfolge der Sperrerfassung in der gesamten Anwendung. Wenn alle Threads zuerst die Sperre mit der niedrigeren Nummer und dann die mit der höheren Nummer erwerben, ist zirkuläres Warten (Circular-Wait-Bedingung) unmöglich. In großen Projekten wird die Reihenfolge dokumentiert und durch Code-Review überprüft.
TryLock ist eine Sperrmethode, die einen Thread nicht unbegrenzt blockiert, sondern false zurückgibt, wenn die Sperre nicht innerhalb einer bestimmten Zeit erworben wird. In Java wird dies über ReentrantLock.tryLock(timeout, TimeUnit) implementiert, in Kotlin Coroutines über Mutex.withLock mit Timeout. Bei Misserfolg gibt der Thread alle erworbenen Ressourcen frei und wiederholt den Vorgang später.
Bankieralgorithmus ist eine von Edsger Dijkstra vorgeschlagene theoretische Methode zur Deadlock-Prävention. Er modelliert die Ressourcenzuweisung als Banktransaktionen: Das System weist keine Ressource zu, wenn dies zu einem unsicheren Zustand (Deadlock) führen könnte. In der Praxis wird der Algorithmus in der mobilen Entwicklung aufgrund der Schwierigkeit, die maximalen Anforderungen von Threads im Voraus zu kennen, selten verwendet, aber seine Prinzipien werden in SQLite-Datenbanken und Dateisystemen genutzt.
Häufig gestellte Fragen
Nein, für eine gegenseitige Blockierung sind mindestens zwei Threads erforderlich. In Single-Thread-Code werden alle Operationen sequenziell ausgeführt, daher ist zirkuläres Warten unmöglich. Deadlock kann jedoch zwischen Prozessen bei Verwendung von Dateisperren oder Interprozess-Semaphoren auftreten.
In Koroutinen tritt Deadlock auf der Ebene von Suspend-Funktionen auf und blockiert nicht den OS-Thread, was es weniger auffällig macht. Mutex aus kotlinx.coroutines ist eine suspendierende Sperre — sie blockiert den Thread nicht, aber die Koroutine wird nicht ausgeführt. Zur Erkennung verwenden Sie DebugProbes aus dem Modul kotlinx-coroutines-debug.
SQLite-Deadlock tritt auf, wenn zwei Datenbankverbindungen versuchen, Transaktionen in unterschiedlicher Reihenfolge auszuführen. SQLite erkennt solche Situationen und gibt den Fehlercode SQLITE_BUSY oder SQLITE_LOCKED zurück. In Android wird empfohlen, Room mit einer einzigen Datenbankinstanz und Transaktionen über @Transaction zu verwenden, was Deadlock zwischen Verbindungen beseitigt.
Android Runtime verfügt über einen integrierten Deadlock-Detektor, der bei der Generierung eines ANR (Application Not Responding) ausgelöst wird. Das System analysiert den Thread Dump aller Anwendungs-Threads und markiert gegenseitige Blockierungen. Das Ergebnis ist unter /data/anr/traces.txt und in der Google Play Console im Bereich ANR Reports verfügbar.
Erstellen Sie zunächst einen Thread Dump aller Anwendungs-Threads. Analysieren Sie, welche Sperren jeder Thread hält und welche zu erwerben er versucht. Implementieren Sie einen Watchdog-Timer mit automatischem Dump bei Überschreitung des Zeitlimits. Fügen Sie nach der Behebung die ThreadSafety-Lint-Regel in Ihre CI-Pipeline ein, um ein erneutes Auftreten zu verhindern.
Zusammenfassung
Wir entwickeln eine mobile Applikation schlüsselfertig
IT Sectr entwickelt seit 2017 iOS- und Android-Apps für Startups und Unternehmen. Wir beraten Sie und schlagen die beste Lösung vor.
Lesen Sie auch