Deadlock (wederzijdse blokkering) — is een toestand waarin twee of meer threads oneindig wachten op het vrijkomen van bronnen die door andere deelnemers zijn ingenomen. Volgens Oracle Java Tutorials (2024), ontstaat Deadlock bij circulair wachten, wanneer elke thread een vergrendeling vasthoudt die een andere thread nodig heeft. Zonder speciale detectiemiddelen stopt Deadlock de uitvoering van de applicatie volledig zonder zichtbare fouten.
Belangrijkste punten
Deadlock (wederzijdse blokkering) — is een situatie in multi-thread programmeren waarin twee of meer threads elkaar permanent blokkeren. Elke thread houdt een bron vast die een andere thread nodig heeft en geeft deze niet vrij, in afwachting van de captatie van de ontbrekende bron. Als gevolg kan geen van de threads de uitvoering voortzetten.
In mobiele ontwikkeling is Deadlock bijzonder kritisch omdat het geen uitzonderingen of crashes veroorzaakt. De applicatie stopt simpelweg met reageren op gebruikersacties (ANR — Application Not Responding) en de enige uitweg is geforceerde beëindiging van het proces. Volgens Google-gegevens (Android Performance Patterns, 2023) is ongeveer 15% van de ANR-rapporten in Google Play Console gerelateerd aan wederzijdse blokkeringen in achtergrondthreads.
Het belangrijkste verschil van Deadlock met andere concurrency-problemen — de onomkeerbaarheid zonder externe interventie. Threads zullen de bronnen niet zelf vrijgeven, omdat de planner van het besturingssysteem de vergrendeling niet geforceerd kan intrekken. Dit onderscheidt Deadlock van Livelock, waar threads actief zijn maar geen nuttig werk verrichten.
In 1971 formuleerde Edward G. Coffman vier verplichte voorwaarden die nodig zijn voor het ontstaan van Deadlock. Als er minstens één ontbreekt, is wederzijdse blokkering onmogelijk. Deze voorwaarden staan bekend als de Coffman-voorwaarden en vormen de basis van alle Deadlock-preventie-algoritmen.
Een bron kan op elk moment slechts door één thread worden ingenomen. Als een bron gelijktijdig lezen door meerdere threads toestaat (bijvoorbeeld ReadWriteLock in leesmodus), ontstaat er geen Deadlock. Deze voorwaarde vloeit voort uit de aard van Mutex en vergrendelingen.
Een thread houdt een reeds ingenomen bron vast en wacht tegelijkertijd op captatie van een andere bron. Als de thread de huidige bron kan vrijgeven voordat de volgende wordt aangevraagd (via two-phase locking), wordt de voorwaarde Hold and Wait geschonden. In Android manifesteert dit zich vaak wanneer een thread de databasevergrendeling vasthoudt en probeert de SharedPreferences-vergrendeling te verkrijgen.
Het besturingssysteem kan de vergrendeling niet geforceerd van de thread afnemen. De bron wordt alleen vrijgegeven wanneer de thread deze zelf loslaat. In sommige systemen (bijvoorbeeld SQLite WAL-modus) is geforceerde preemptie geïmplementeerd op het niveau van individuele operaties, wat het risico op Deadlock vermindert.
Er bestaat een gesloten keten van threads, waarbij elke thread wacht op een bron die wordt vastgehouden door de volgende in de keten. Bijvoorbeeld, thread A houdt bron 1 vast en wacht op bron 2, thread B houdt bron 2 vast en wacht op bron 1. Dit is de enige voorwaarde die de ontwikkelaar architectonisch kan elimineren — via een hiërarchie van vergrendelingen. Als alle threads bronnen in een strikt gedefinieerde globale volgorde captiveren, is een cyclus fysiek onmogelijk.
In de praktijk ontstaat Deadlock in Android-applicaties meestal door impliciete kruising van vergrendelingen van verschillende niveaus: de databasevergrendeling (Room), de SharedPreferences-vergrendeling en de collectievergrendeling in het geheugen. Elk van deze vergrendelingen wordt beheerd door verschillende componenten, en zonder een gecentraliseerd protocol voor de captatievolgorde creëren ontwikkelaars onbedoeld cycli.
Laten we een klassiek voorbeeld van wederzijdse blokkering bekijken — twee threads captiveren vergrendelingen in verschillende volgorde. Als de eerste thread bron A blokkeert en probeert B te captiveren, en de tweede blokkeert B en probeert A te captiveren, ontstaat er een Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // simulatie van werk
synchronized(lockB) {
println("operationA uitgevoerd")
}
}
}
fun operationB() {
synchronized(lockB) { // omgekeerde volgorde!
Thread.sleep(50)
synchronized(lockA) {
println("operationB uitgevoerd")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Applicatie zal voor altijd vastlopen — Deadlock!
}
In dit voorbeeld captiveert operationA lockA, en operationB captiveert lockB. Vervolgens probeert elk de tweede vergrendeling te captiveren — en beide wachten oneindig. Het programma loopt vast zonder foutmelding. De enige manier om dit op te lossen is het garanderen van dezelfde volgorde van vergrendelingscaptatie in alle methoden.
Deze drie concurrency-problemen worden vaak verward, maar hun mechanismen en gevolgen zijn fundamenteel verschillend. Deadlock — volledige stop, Starvation — oneindig wachten op een bron, Livelock — actieve inactiviteit. Het begrijpen van de verschillen is cruciaal voor het kiezen van de juiste eliminatiestrategie.
| Kenmerk | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Thread-status | Geblokkeerd (BLOCKED) | Gereed (RUNNABLE) | Actief (RUNNABLE) |
| Uitvoering van werk | Nee | Nee | Ja, maar nutteloos |
| Oorzaak | Circulair wachten | Oneerlijke planning | Onjuiste conflictbehandeling |
| Detectie | Thread Dump, time-outs | Voortgangsbewaking | Pogingenteller |
Starvation (uithongering) treedt op wanneer de planner de uitvoering van een thread met lage prioriteit constant uitstelt ten gunste van anderen. In tegenstelling tot Deadlock is de thread niet geblokkeerd — hij is klaar voor uitvoering, maar krijgt geen processortijd. In Android is het typische scenario een achtergrondthread met lage prioriteit die nooit wordt uitgevoerd als de UI- en Servicethreads constant actief zijn.
Livelock (actieve blokkering) — een situatie waarin threads niet geblokkeerd zijn, maar oneindig reageren op elkaars acties, zonder nuttig werk te verrichten. De klassieke analogie — twee mensen komen elkaar tegen in een gang en beiden proberen plaats te maken, terwijl ze dezelfde kant op bewegen. In tegenstelling tot Deadlock verbruiken threads in Livelock CPU, waardoor de batterij van het apparaat leegraakt.
Thread Dump — het belangrijkste hulpmiddel voor het detecteren van wederzijdse blokkeringen in JVM en Android Runtime. Bij een dump analyseert JVM automatisch de afhankelijkheidsgraaf tussen monitoren en markeert Deadlock-cycli. In Android Studio kan de threaddump worden verkregen via Android Profiler of het commando kill -3 PID vanuit ADB Shell.
Automatische detectie van Deadlock tijdens uitvoering wordt gerealiseerd via Watchdog-timers. Als een thread de bewerking niet binnen een bepaalde timeout voltooit, initieert de watchdog het maken van een dump en stuurt een rapport naar het Crash Reporting-systeem (Firebase Crashlytics, Sentry). Volgens Sentry-gegevens (Issue Resolution Report, 2024) verkort het configureren van een watchdog de diagnostiektijd van Deadlock van weken tot enkele uren.
In de ontwikkelingsfase zijn de statische analysator ThreadSafe van JetBrains en Checker Framework met Lock Checker-module effectief. Deze tools analyseren de volgorde van vergrendelingscaptatie op het niveau van broncode en waarschuwen voor potentiële cycli. Aanvullend wordt Test-Driven Deadlock Detection aanbevolen — stresstests die bewerkingen met verschillende vergrendelingsvolgorden in honderden threads starten.
Speciale aandacht verdient Cooperative Deadlock Detection — een methode waarbij threads informatie over ingenomen vergrendelingen uitwisselen via een wereldwijd register. Als een thread een potentiële cyclus detecteert, geeft hij alle bronnen vrij en herhaalt de bewerking. Deze aanpak wordt gebruikt in gedistribueerde systemen (Apache ZooKeeper, Google Chubby) en wordt geleidelijk geïntroduceerd in mobiele ontwikkeling via bibliotheken zoals Jetpack Sync.
De meest betrouwbare manier — het vaststellen van een globale volgorde van vergrendelingscaptatie in de hele applicatie. Als alle threads eerst de vergrendeling met het kleinere nummer captiveren en daarna die met het grotere nummer, is circulair wachten (Circular Wait-voorwaarde) onmogelijk. In grote projecten wordt de volgorde vastgelegd in documentatie en gecontroleerd via code review.
TryLock — een vergrendelingsmethode die de thread niet oneindig blokkeert, maar false retourneert als de vergrendeling niet binnen een bepaalde tijd is verkregen. In Java wordt dit geïmplementeerd via ReentrantLock.tryLock(timeout, TimeUnit), in Kotlin Coroutines via Mutex.withLock met een timeout. Bij mislukking geeft de thread alle ingenomen bronnen vrij en probeert het later opnieuw.
Het bankieralgoritme — een theoretische methode voor Deadlock-preventie, voorgesteld door Edsger Dijkstra. Het modelleert de distributie van bronnen als banktransacties: het systeem kent geen bron toe als dit tot een onveilige toestand (deadlock) zou kunnen leiden. In de praktijk wordt het algoritme zelden toegepast in mobiele ontwikkeling vanwege de complexiteit van het vooraf kennen van de maximale behoeften van threads, maar de principes ervan worden gebruikt in SQLite-databases en bestandssystemen.
Veelgestelde vragen
Nee, voor wederzijdse blokkering zijn minimaal twee threads nodig. In single-thread code worden alle bewerkingen sequentieel uitgevoerd, dus circulair wachten is onmogelijk. Deadlock kan echter optreden tussen processen bij het gebruik van bestandsvergrendelingen of inter-proces semaforen.
In coroutines ontstaat Deadlock op het niveau van opgeschorte functies (suspend) en blokkeert het de OS-thread niet, wat het minder opvallend maakt. Mutex uit kotlinx.coroutines is opschortend (suspending), het blokkeert de thread niet, maar de coroutine wordt niet uitgevoerd. Gebruik voor detectie DebugProbes uit de module kotlinx-coroutines-debug.
SQLite Deadlock treedt op wanneer twee databaseverbindingen proberen transacties in verschillende volgorden uit te voeren. SQLite detecteert dergelijke situaties en retourneert de foutcode SQLITE_BUSY of SQLITE_LOCKED. In Android wordt aanbevolen Room te gebruiken met een enkele instantie van de database en transacties via @Transaction, wat de Deadlock tussen verbindingen elimineert.
Android Runtime heeft een ingebouwde Deadlock-detector die wordt geactiveerd bij het genereren van ANR (Application Not Responding). Het systeem analyseert de Thread Dump van alle threads van de applicatie en markeert wederzijdse blokkeringen. Het resultaat is beschikbaar in /data/anr/traces.txt en Google Play Console in de sectie ANR Reports.
Verkrijg eerst de Thread Dump van alle threads van de applicatie. Analyseer welke vergrendelingen elke thread vasthoudt en welke hij probeert te captiveren. Implementeer een Watchdog-timer met automatische dump bij overschrijding van de tijdslimiet. Voeg na de reparatie een lint-regel ThreadSafety toe in de CI-pijplijn om herhaling te voorkomen.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook