Deadlock (ömsesidig blockering) — är ett tillstånd där två eller fler trådar oändligt väntar på frigörande av resurser som tagits av andra deltagare. Enligt Oracle Java Tutorials (2024), uppstår Deadlock vid cirkulär väntan, när varje tråd håller ett lås som en annan tråd behöver. Utan speciella detektionsverktyg stoppar Deadlock helt exekveringen av applikationen utan synliga fel.
Huvudpunkter
Deadlock (ömsesidig blockering) — är en situation inom flertrådig programmering där två eller fler trådar permanent blockerar varandra. Varje tråd håller en resurs som en annan tråd behöver och frigör den inte, i väntan på att fånga den saknade resursen. Som ett resultat kan ingen av trådarna fortsätta exekveringen.
Inom mobil utveckling är Deadlock särskilt kritiskt eftersom det inte orsakar undantag eller krascher. Applikationen slutar helt enkelt att svara på användaråtgärder (ANR — Application Not Responding), och den enda utvägen är tvungen avslutning av processen. Enligt Google-data (Android Performance Patterns, 2023) är cirka 15% av ANR-rapporterna i Google Play Console relaterade till ömsesidiga blockeringar i bakgrundstrådar.
Den viktigaste skillnaden mellan Deadlock och andra konkurrensproblem — dess oåterkallelighet utan extern intervention. Trådar kommer inte att frigöra resurserna själva, eftersom operativsystemets schemaläggare inte kan återkalla låset med tvång. Detta skiljer Deadlock från Livelock, där trådar är aktiva men inte utför användbart arbete.
År 1971 formulerade Edward G. Coffman fyra obligatoriska villkor som är nödvändiga för uppkomsten av Deadlock. Om åtminstone ett saknas, är ömsesidig blockering omöjlig. Dessa villkor är kända som Coffmans villkor och ligger till grund för alla algoritmer för Deadlock-förebyggande.
En resurs kan fångas endast av en tråd vid varje given tidpunkt. Om en resurs tillåter samtidig läsning av flera trådar (till exempel ReadWriteLock i läsläge), uppstår ingen Deadlock. Detta villkor härrör från själva naturen av Mutex och lås.
En tråd håller en redan fångad resurs och väntar samtidigt på fångst av en annan resurs. Om tråden kan frigöra den aktuella resursen innan den begär nästa (genom tvåfaslåsning), bryts villkoret Hold and Wait. I Android visar sig detta ofta när en tråd håller databaslåset och försöker fånga SharedPreferences-låset.
Operativsystemet kan inte med tvång ta bort låset från en tråd. Resursen frigörs endast när tråden själv släpper den. I vissa system (till exempel SQLite WAL-läge) är tvångsföregripning implementerad på nivån av enskilda operationer, vilket minskar risken för Deadlock.
Det finns en sluten kedja av trådar, där varje väntar på en resurs som hålls av nästa i kedjan. Till exempel, tråd A håller resurs 1 och väntar på resurs 2, tråd B håller resurs 2 och väntar på resurs 1. Detta är det enda villkoret som utvecklaren kan eliminera arkitektoniskt — genom en hierarki av lås. Om alla trådar fångar resurser i en strikt fastställd global ordning, är en cykel fysiskt omöjlig.
I praktiken, i Android-applikationer, uppstår Deadlock oftast på grund av implicit korsning av lås på olika nivåer: databaslås (Room), SharedPreferences-lås och samlingslås i minnet. Var och en av dessa lås hanteras av olika komponenter, och utan ett centraliserat protokoll för fångstordning skapar utvecklare oavsiktligt cykler.
Låt oss titta på ett klassiskt exempel på ömsesidig blockering — två trådar fångar lås i olika ordning. Om den första tråden blockerar resurs A och försöker fånga B, och den andra blockerar B och försöker fånga A, uppstår Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // simulering av arbete
synchronized(lockB) {
println("operationA utförd")
}
}
}
fun operationB() {
synchronized(lockB) { // omvänd ordning!
Thread.sleep(50)
synchronized(lockA) {
println("operationB utförd")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// Applikationen kommer att frysa för alltid — Deadlock!
}
I detta exempel fångar operationA lockA, och operationB fångar lockB. Sedan försöker var och en fånga det andra låset — och båda väntar oändligt. Programmet fryser utan felmeddelande. Det enda sättet att åtgärda är att garantera samma ordning för låsfångst i alla metoder.
Dessa tre konkurrensproblem förväxlas ofta, men deras mekanismer och konsekvenser är fundamentalt olika. Deadlock — fullständigt stopp, Starvation — oändlig väntan på en resurs, Livelock — aktiv overksamhet. Att förstå skillnaderna är avgörande för att välja rätt elimineringsstrategi.
| Egenskap | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Trådarnas tillstånd | Blockerade (BLOCKED) | Redo (RUNNABLE) | Aktiva (RUNNABLE) |
| Utförande av arbete | Nej | Nej | Ja, men oanvändbart |
| Orsak | Cirkulär väntan | Orättvis schemaläggning | Felaktig konflikthantering |
| Detektering | Thread Dump, tidsgränser | Övervakning av framsteg | Räknare för återförsök |
Starvation (svältning) uppstår när schemaläggaren ständigt skjuter upp exekveringen av en lågprioriterad tråd till förmån för andra. Till skillnad från Deadlock är tråden inte blockerad — den är redo för exekvering men får ingen CPU-tid. I Android är det typiska scenariot en lågprioriterad bakgrundstråd som aldrig körs om UI- och Servicetrådar är ständigt aktiva.
Livelock (aktiv blockering) — en situation där trådar inte är blockerade, men oändligt reagerar på varandras handlingar, utan att utföra användbart arbete. Den klassiska analogin — två personer möts i en korridor och båda försöker lämna väg, medan de rör sig åt samma håll. Till skillnad från Deadlock förbrukar trådar i Livelock CPU, vilket dränerar enhetens batteri.
Thread Dump (tråddump) — det huvudsakliga verktyget för att detektera ömsesidiga blockeringar i JVM och Android Runtime. Vid dumpning analyserar JVM automatiskt beroendegrafen mellan monitorer och markerar Deadlock-cykler. I Android Studio kan tråddumpen erhållas via Android Profiler eller kommandot kill -3 PID från ADB Shell.
Automatisk detektering av Deadlock under exekvering realiseras genom Watchdog-timer. Om en tråd inte slutför en operation inom en angiven timeout, initierar watchdog skapandet av en dump och skickar en rapport till krashrapporteringssystemet (Firebase Crashlytics, Sentry). Enligt Sentry-data (Issue Resolution Report, 2024) minskar konfigurering av watchdog tiden för diagnos av Deadlock från veckor till några timmar.
I utvecklingsfasen är statiska analysatorn ThreadSafe från JetBrains och Checker Framework med Lock Checker-modulen effektiva. Dessa verktyg analyserar ordningen för låsfångst på källkodsnivå och varnar för potentiella cykler. Dessutom rekommenderas Test-Driven Deadlock Detection — stresstester som kör operationer med olika låsningsordningar i hundratals trådar.
Särskild uppmärksamhet förtjänar Cooperative Deadlock Detection — en metod där trådar utbyter information om fångade lås via ett globalt register. Om en tråd detekterar en potentiell cykel, frigör den alla resurser och upprepar operationen. Detta tillvägagångssätt används i distribuerade system (Apache ZooKeeper, Google Chubby) och introduceras gradvis i mobil utveckling genom bibliotek som Jetpack Sync.
Det mest tillförlitliga sättet — att fastställa en global ordning för låsfångst i hela applikationen. Om alla trådar först fångar låset med det mindre numret, sedan med det större, är cirkulär väntan (Circular Wait-villkoret) omöjlig. I stora projekt fastställs ordningen i dokumentation och verifieras genom kodgranskning.
TryLock — en låsningsmetod som inte blockerar tråden oändligt, utan returnerar false om låset inte erhålls inom en angiven tid. I Java implementeras detta genom ReentrantLock.tryLock(timeout, TimeUnit), i Kotlin Coroutines genom Mutex.withLock med timeout. Vid misslyckande frigör tråden alla fångade resurser och försöker igen senare.
Bankir-algoritmen — en teoretisk metod för Deadlock-förebyggande, föreslagen av Edsger Dijkstra. Den modellerar distributionen av resurser som banktransaktioner: systemet tilldelar inte en resurs om detta skulle kunna leda till ett osäkert tillstånd (deadlock). I praktiken tillämpas algoritmen sällan inom mobil utveckling på grund av komplexiteten i att i förväg känna till trådarnas maximala behov, men dess principer används i SQLite-databaser och filsystem.
Vanliga frågor
Nej, för ömsesidig blockering krävs minst två trådar. I enkeltrådad kod utförs alla operationer sekventiellt, så cirkulär väntan är omöjlig. Deadlock kan dock uppstå mellan processer vid användning av fillås eller interprocess-semaforer.
I korutiner uppstår Deadlock på nivån av avstängda funktioner (suspend) och blockerar inte OS-tråden, vilket gör det mindre märkbart. Mutex från kotlinx.coroutines är avstängande (suspending), det blockerar inte tråden, men korutinen körs inte. För detektering, använd DebugProbes från modulen kotlinx-coroutines-debug.
SQLite Deadlock uppstår när två databasanslutningar försöker utföra transaktioner i olika ordning. SQLite detekterar sådana situationer och returnerar felkoden SQLITE_BUSY eller SQLITE_LOCKED. I Android rekommenderas att använda Room med en enda instans av databasen och transaktioner genom @Transaction, vilket eliminerar Deadlock mellan anslutningar.
Android Runtime har en inbyggd Deadlock-detektor som aktiveras vid generering av ANR (Application Not Responding). Systemet analyserar Thread Dump av alla applikationens trådar och markerar ömsesidiga blockeringar. Resultatet finns tillgängligt i /data/anr/traces.txt och Google Play Console i avsnittet ANR Reports.
Först, erhåll Thread Dump av alla applikationens trådar. Analysera vilka lås varje tråd håller och vilka den försöker fånga. Implementera en Watchdog-timer med automatisk dump vid överskridande av tidsgränsen. Efter åtgärd, lägg till en lint-regel ThreadSafety i CI-pipelinen för att förebygga återfall.
Sammanfattning
Vi utvecklar en mobil applikation nyckelfärdigt
IT Sectr skapar iOS- och Android-applikationer för startups och företag sedan 2017. Vi ger dig råd och föreslår den bästa lösningen.
Läs också