Deadlock v mobilním vývoji: co to je, příčiny vzniku a způsoby, jak se vyhnout vzájemnému blokování

Autor: IT Sectr Publikováno: 2026-03-18 Doba čtení: 10 min

Deadlock (vzájemné blokování) — je stav, ve kterém dvě nebo více vláken nekonečně čekají na uvolnění zdrojů obsazených jinými účastníky. Podle Oracle Java Tutorials (2024), Deadlock vzniká při kruhovém čekání, když každé vlákno drží zámek potřebný jinému vláknu. Bez speciálních prostředků detekce Deadlock úplně zastaví provádění aplikace bez viditelných chyb.

Hlavní body

  • Deadlock — vzájemné blokování vláken, při kterém každé čeká na zdroj obsazený jiným vláknem
  • Čtyři podmínky Coffman (Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait) jsou nutné pro vznik Deadlocku
  • Deadlock se liší od Starvation tím, že vlákna nejsou blokována, ale aktivně čekají v cyklické závislosti
  • Thread Dump — hlavní nástroj detekce Deadlocku v JVM a Android Runtime
  • Hierarchie zámků a jednotné pořadí získávání zdrojů — hlavní způsob prevence vzájemných blokování

Co je Deadlock?

Deadlock (vzájemné blokování) — je situace v multithreadingovém programování, kde dvě nebo více vláken trvale blokují navzájem. Každé vlákno drží zdroj potřebný jinému vláknu a neuvolňuje jej, čekající na získání chybějícího zdroje. Výsledkem je, že žádné z vláken nemůže pokračovat v provádění.

V mobilním vývoji je Deadlock obzvláště kritický, protože nezpůsobuje výjimky nebo pády. Aplikace jednoduše přestane reagovat na akce uživatele (ANR — Application Not Responding), a jediným východiskem je nucené ukončení procesu. Podle údajů Google (Android Performance Patterns, 2023) asi 15% hlášení ANR v Google Play Console souvisí se vzájemnými blokováními na pozadí.

Klíčový rozdíl Deadlocku od jiných problémů konkurence — jeho nevratnost bez vnějšího zásahu. Vlákna neuvolní zdroje sama, protože plánovač operačního systému nemůže nuceně odebrat zámek. To odlišuje Deadlock od Livelocku, kde jsou vlákna aktivní, ale nevykonávají užitečnou práci.

Podmínky vzniku Deadlocku

V roce 1971 Edward G. Coffman formuloval čtyři povinné podmínky nutné pro vznik Deadlocku. Pokud alespoň jedna chybí, vzájemné blokování je nemožné. Tyto podmínky jsou známy jako Coffmanovy podmínky a tvoří základ všech algoritmů prevence Deadlocku.

Vzájemné vyloučení (Mutual Exclusion)

Zdroj může být získán pouze jedním vláknem v každém okamžiku. Pokud zdroj umožňuje současné čtení více vlákny (například ReadWriteLock v režimu čtení), Deadlock nevzniká. Tato podmínka vyplývá z podstaty Mutexu a zámků.

Držení a čekání (Hold and Wait)

Vlákno drží již získaný zdroj a současně čeká na získání jiného zdroje. Pokud vlákno může uvolnit aktuální zdroj před požadavkem na další (prostřednictvím dvoufázového zamykání), podmínka Hold and Wait je porušena. V Androidu se to často projevuje, když vlákno drží zámek databáze a snaží se získat zámek SharedPreferences.

Absence nuceného převzetí (No Preemption)

Operační systém nemůže nuceně odebrat zámek vláknu. Zdroj je uvolněn pouze tehdy, když jej vlákno samo uvolní. V některých systémech (například SQLite WAL režim) je nucené převzetí implementováno na úrovni jednotlivých operací, což snižuje riziko Deadlocku.

Kruhové čekání (Circular Wait)

Existuje uzavřený řetězec vláken, z nichž každé čeká na zdroj držený následujícím v řetězci. Například, vlákno A drží zdroj 1 a čeká na zdroj 2, vlákno B drží zdroj 2 a čeká na zdroj 1. Toto je jediná podmínka, kterou může vývojář architektonicky odstranit — pomocí hierarchie zámků. Pokud všechna vlákna získávají zdroje v přísně stanoveném globálním pořadí, cyklus je fyzicky nemožný.

V praxi v aplikacích pro Android Deadlock nejčastěji vzniká kvůli implicitnímu křížení zámků různých úrovní: zámku databáze (Room), zámku SharedPreferences a zámku kolekce v paměti. Každý z těchto zámků je řízen různými komponentami a bez centralizovaného protokolu pořadí získávání vývojáři neúmyslně vytvářejí cykly.

Příklad Deadlocku v kódu Kotlin

Podívejme se na klasický příklad vzájemného blokování — dvě vlákna získávají zámky v různém pořadí. Pokud první vlákno blokuje zdroj A a snaží se získat B, a druhé blokuje B a snaží se získat A, vzniká Deadlock.

kotlin
class DeadlockExample {
    private val lockA = Any()
    private val lockB = Any()

    fun operationA() {
        synchronized(lockA) {
            Thread.sleep(50)  // simulace práce
            synchronized(lockB) {
                println("operationA provedena")
            }
        }
    }

    fun operationB() {
        synchronized(lockB) {  // obrácené pořadí!
            Thread.sleep(50)
            synchronized(lockA) {
                println("operationB provedena")
            }
        }
    }
}

fun main() {
    val ex = DeadlockExample()
    Thread { ex.operationA() }.start()
    Thread { ex.operationB() }.start()
    // Aplikace navždy zamrzne — Deadlock!
}

V tomto příkladě operationA získává lockA a operationB získává lockB. Poté se každé snaží získat druhý zámek — a obě nekonečně čekají. Program zamrzne bez chybové zprávy. Jediným způsobem nápravy je zaručit stejné pořadí získávání zámků ve všech metodách.

Deadlock vs Starvation vs Livelock

Tyto tři problémy konkurence jsou často zaměňovány, ale jejich mechanismy a důsledky jsou zásadně odlišné. Deadlock — úplné zastavení, Starvation — nekonečné čekání na zdroj, Livelock — aktivní nečinnost. Pochopení rozdílů je kritické pro výběr správné strategie odstranění.

CharakteristikaDeadlockStarvationLivelock
Stav vlákenBlokována (BLOCKED)Připravena (RUNNABLE)Aktivní (RUNNABLE)
Provádění práceNeNeAno, ale neužitečná
PříčinaKruhové čekáníNespravedlivé plánováníNesprávné řešení konfliktu
DetekceThread Dump, časové limitySledování pokrokuPoĄet opakování

Starvation (hladovění) vzniká, když plánovač neustále odkládá provedení nízkoprioritního vlákna ve prospěch jiných. Na rozdíl od Deadlocku vlákno není blokováno — je připraveno k provedení, ale nezískává čas CPU. V Androidu je typickým scénářem nízkoprioritní vlákno na pozadí, které se nikdy neprovede, pokud jsou vlákna UI a Service neustále aktivní.

Livelock (aktivní blokování) — situace, kdy vlákna nejsou blokována, ale nekonečně reagují na vzájemné akce, aniž by vykonávala užitečnou práci. Klasická analogie — dva lidé se potkají na chodbě a oba se snaží uhnout z cesty, přičemž se pohybují stejným směrem. Na rozdíl od Deadlocku vlákna v Livelocku spotřebovávají CPU, čímž vybíjejí baterii zařízení.

Jak detekovat Deadlock

Thread Dump (výdpis vláken) — hlavní nástroj detekce vzájemných blokování v JVM a Android Runtime. Při výdpisu JVM automaticky analyzuje graf závislostí mezi monitory a označuje cykly Deadlocku. V Android Studio lze výdpis vláken získat prostřednictvím Android Profiler nebo příkazem kill -3 PID z ADB Shell.

Automatická detekce Deadlocku během provádění je realizována pomocí Watchdog časovačů. Pokud vlákno nedokončí operaci ve stanoveném časovém limitu, watchdog zahájí vytvoření výdpisu a odešle zprávu do systému hlášení chyb (Firebase Crashlytics, Sentry). Podle údajů Sentry (Issue Resolution Report, 2024) konfigurace watchdogu zkracuje dobu diagnostiky Deadlocku z týdnů na několik hodin.

Ve fázi vývoje jsou účinné statický analyzátor ThreadSafe od JetBrains a Checker Framework s modulem Lock Checker. Tyto nástroje analyzují pořadí získávání zámků na úrovni zdrojového kódu a varují před potenciálními cykly. Doplňkově se doporučuje Test-Driven Deadlock Detection — zátěžové testy, které spouštějí operace s různým pořadím zamykání ve stovkách vláken.

Zvláštní pozornost si zaslouží Cooperative Deadlock Detection — metoda, při které si vlákna vyměňují informace o získaných zámcích prostřednictvím globálního registru. Pokud vlákno detekuje potenciální cyklus, uvolní všechny zdroje a zopakuje operaci. Tento přístup se používá v distribuovaných systémech (Apache ZooKeeper, Google Chubby) a postupně se zavádí do mobilního vývoje prostřednictvím knihoven jako Jetpack Sync.

Metody prevence vzájemného blokování

Hierarchie zámků (Lock Ordering)

Nejspolehlivější způsob — stanovení globálního pořadí získávání zámků v celé aplikaci. Pokud všechna vlákna nejprve získají zámek s nižším číslem a poté s vyšším, kruhové čekání (podmínka Circular Wait) je nemožné. Ve velkých projektech je pořadí stanoveno v dokumentaci a ověřováno pomocí code review.

TryLock s časovým limitem

TryLock — metoda zamykání, která neblokuje vlákno nekonečně, ale vrací false, pokud zámek není získán ve stanoveném čase. V Javě je to implementováno pomocí ReentrantLock.tryLock(timeout, TimeUnit), v Kotlin Coroutines pomocí Mutex.withLock s časovým limitem. Při neúspěchu vlákno uvolní všechny získané zdroje a zkusí to později.

Bankéřův algoritmus (Banker's Algorithm)

Bankéřův algoritmus — teoretická metoda prevence Deadlocku navržená Edsgerem Dijkstrou. Modeluje distribuci zdrojů jako bankovní transakce: systém nepřiděluje zdroj, pokud by to mohlo vést k nebezpečnému stavu (deadlock). V praxi se algoritmus v mobilním vývoji používá zřídka kvůli složitosti předběžné znalosti maximálních potřeb vláken, ale jeho principy se používají v databázích SQLite a souborových systémech.

Často kladené otázky

Může Deadlock vzniknout v jednovláknové aplikaci?

Ne, pro vzájemné blokování jsou potřeba alespoň dvě vlákna. V jednom vláknu jsou všechny operace prováděny sekvenčně, takže kruhové čekání je nemožné. Deadlock však může nastat mezi procesy při použití zámků souborů nebo meziprocesorových semaforů.

Čím se liší Deadlock v Kotlin Coroutines od Deadlocku ve vláknech?

V korutinách Deadlock vzniká na úrovni pozastavených funkcí (suspend) a neblokuje vlákno OS, což ho činí méně patrným. Mutex z kotlinx.coroutines je pozastavující (suspending), neblokuje vlákno, ale korutina se neprovede. K detekci použijte DebugProbes z modulu kotlinx-coroutines-debug.

Co je Deadlock v SQLite na Androidu?

Deadlock v SQLite vzniká, když dvě připojení k databázi se snaží provést transakce v různém pořadí. SQLite takové situace detekuje a vrací chybový kód SQLITE_BUSY nebo SQLITE_LOCKED. V Androidu se doporučuje používat Room s jedinou instancí databáze a transakcemi přes @Transaction, což eliminuje Deadlock mezi připojeními.

Jak Android detekuje Deadlock?

Android Runtime má vestavěný detektor Deadlocku, který se spouští při generování ANR (Application Not Responding). Systém analyzuje Thread Dump všech vláken aplikace a označuje vzájemná blokování. Výsledek je k dispozici v /data/anr/traces.txt a Google Play Console v sekci ANR Reports.

Co dělat, když je Deadlock nalezen v produkci?

Nejprve získejte Thread Dump všech vláken aplikace. Analyzujte, jaké zámky každé vlákno drží a jaké se snaží získat. Implementujte Watchdog časovač s automatickým výdpisem při překročení časového limitu. Po opravě přidejte lint pravidlo ThreadSafety do CI pipeline pro prevenci recidivy.

Shrnutí

  • Deadlock — vzájemné blokování, při kterém vlákna nekonečně čekají na zdroje obsazené navzájem
  • Čtyři Coffmanovy podmínky (Mutual Exclusion, Hold and Wait, No Preemption, Circular Wait) jsou nutné pro vznik Deadlocku
  • Thread Dump — standardní metoda detekce vzájemných blokování v JVM a Android Runtime
  • Hierarchie zámků s jednotným globálním pořadím zcela eliminuje podmínku kruhového čekání
  • TryLock s časovým limitem zabraňuje nekonečnému čekání a umožňuje vláknu správně zpracovat nedostupnost zdroje
  • Deadlock vs Starvation — při Deadlocku jsou vlákna blokována, při Starvation jsou připravena k provedení, ale nezískávají CPU
  • Watchdog časovače a statické analyzátory (ThreadSafe, Checker Framework) — základní ochrana proti Deadlocku v CI/CD

Vyvineme mobilní aplikaci na klíč

IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.

Prodiskutovat projekt

Přečtěte si také