Starvation (hladovění vlákna) — je situace, kdy vlákno nezíská přístup k prostředku potřebnému pro pokračování práce, ačkoli je samo připraveno k vykonání. Podle Baeldung (Java Thread Starvation, 2024) vzniká hladovění kvůli nespravedlivému plánování, kdy jsou vlákna s nízkou prioritou neustále odkládána ve prospěch vláken s vyšší prioritou. Na rozdíl od Deadlock, Starvation vlákno neblokuje — zůstává ve stavu RUNNABLE, ale nikdy nezíská čas procesoru.
Hlavní body
Starvation (hladovění vlákna) — je problém vícevláknového programování, při kterém vlákno nemůže získat přístup k prostředku potřebnému pro provedení úkolu, ačkoli prostředek není trvale blokován jiným vláknem. Vlákno je ve stavu RUNNABLE, ale plánovač nebo synchronizační mechanismus systematicky odkládá jeho provedení ve prospěch jiných vláken.
V mobilním vývoji se Starvation projevuje jako nerovnoměrné provádění úkolů: některé operace se provádějí okamžitě, jiné — s katastrofálními zpožděními. Například vlákno na pozadí synchronizující data může nikdy nezískat přístup k databázi, pokud ho vlákno UI a obsluha animací neustále předbíhají. Podle Android Developer Blog (Performance Matters, 2023) je asi 12 % případů zmeškaných snímků (jank) na Androidu způsobeno Starvation úloh na pozadí, na kterých závisí vykreslování.
Klíčový rozdíl Starvation od Deadlock — vratnost. Pokud se zatížení systému sníží nebo se priority přerozdělí, hladovějící vlákno může získat prostředek a dokončit práci. V podmínkách trvale vysokého zatížení však může Starvation trvat neurčitě dlouho a vytvářet dojem zamrzlé aplikace.
synchronized v Javě a Kotlinu — klasický příklad nespravedlivého mechanismu. Při vysoké konkurenci může JVM dávat zámek donekonečna stejným aktivním vláknům, zatímco jiná vlákna neustále prohrávají v závodě. To není chyba JVM, ale vlastnost implementace: nespravedlivé zámky poskytují vyšší propustnost na úkor rovnoměrnosti přístupu. Pro mobilní aplikace se 4-8 vlákny je tento problém obzvláště relevantní.
Nastavení různých priorit vláken může vést k Starvation vláken s nízkou prioritou. V Android Runtime plánovač CFS (Completely Fair Scheduler) Linuxu rozděluje čas procesoru proporcionálně prioritám, a pokud jsou vlákna s vysokou prioritou neustále aktivní, vlákna s nízkou prioritou nemusí nikdy získat CPU. Google kategoricky nedoporučuje měnit priority vláken v Androidu — systém je řídí sám.
Pokud vlákno drží zámek příliš dlouho (provádí těžké výpočty, síťové požadavky nebo operace se soubory uvnitř bloku synchronized), ostatní vlákna čekající na tento zámek hladoví. To je obzvláště nebezpečné v Androidu, kde dlouhé operace ve vlákně UI způsobují ANR a jejich přesun do vláken na pozadí bez optimalizace kritických sekcí přenáší problém Starvation na pracovní vlákna.
Uvažujme příklad, kdy jedno vlákno přebírá zámek příliš často kvůli nespravedlivému plánování. Starvation je demonstrováno nekonečnou smyčkou vlákna s vysokou prioritou, které nedovoluje vláknu s nízkou prioritou přístup ke sdílenému prostředku.
class SharedResource {
private val lock = Any()
fun criticalSection(id: String) {
synchronized(lock) {
println("$id získal přístup")
Thread.sleep(10) // simulace práce
}
}
}
fun main() {
val resource = SharedResource()
// Vlákno s vysokou prioritou — neustále aktivní
val highPriority = Thread {
while (true) {
resource.criticalSection("High")
}
}
// Vlákno s nízkou prioritou — může nikdy nezískat přístup
val lowPriority = Thread {
while (true) {
resource.criticalSection("Low")
}
}
highPriority.start()
lowPriority.start()
// „Low" možná nikdy nevypsat zprávu — Starvation!
}
V tomto příkladu vlákno highPriority neustále přebírá zámek a uvolňuje ho pouze na 10 ms. Kvůli nespravedlivé povaze synchronized bude plánovač JVM s vysokou pravděpodobností dávat zámek znovu stejnému vláknu, které ho právě uvolnilo — vlákno s nízkou prioritou hladoví. Řešením je použít ReentrantLock(true) s příznakem fair, který zaručuje pořadí ve frontě čekání.
Opravená verze s fair lock zajišťuje spravedlivé rozdělení přístupu k prostředku.
class FairSharedResource {
private val fairLock = ReentrantLock(true) // fair = true
fun criticalSection(id: String) {
fairLock.lock()
try {
println("$id získal přístup (fair)")
Thread.sleep(10)
} finally {
fairLock.unlock()
}
}
}
Tři klasické problémy vícevláknového programování — Starvation, Deadlock a Livelock — jsou často spojovány, ale jejich mechanismy a způsoby odstranění se liší. Starvation — vlákno je připraveno, ale nezíská prostředek. Deadlock — vlákna jsou blokována cyklickým čekáním. Livelock — vlákna jsou aktivní, ale nepostupují.
| Parametr | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Stav vlákna | RUNNABLE | BLOCKED | RUNNABLE |
| Pokrok | Ne | Ne | Ne (i když aktivní) |
| Spotřeba CPU | Nízká | Minimální | Vysoká (až 100%) |
| Příčina | Nespravedlivé plánování | Cyklické čekání | Stejná reakce na konflikt |
| Hlavní řešení | Fair Lock, zkrácení kritických sekcí | Hierarchie zámků | Limit opakování, exponenciální backoff |
Starvation je považováno za méně kritické než Deadlock, protože není fatální — při snížení zátěže se hladovějící vlákno nakonec provede. V podmínkách reálného používání Android aplikací, kde je paměť a CPU omezená, však může Starvation trvat minuty a vytvářet nepřijatelný UX.
Thread Dump s opakovanými snímky v krátkých intervalech — základní metoda detekce hladovění. Pokud je vlákno konzistentně ve stavu RUNNABLE, ale jeho zásobník volání se nemění během několika snímků — to je klasický znak Starvation. V Android Studiu se k tomu používá Android Profiler se záznamem stavu vláken v čase.
Automatizovaná detekce je možná prostřednictvím monitorování doby provádění úkolů. Pokud se úkol s předvídatelnou dobou provádění (např. 50 ms) provádí 5 sekund nebo déle — existuje vysoká pravděpodobnost Starvation. V mobilních aplikacích Firebase Performance Monitoring umožňuje konfigurovat vlastní stopy (custom traces) pro kritické sekce a dostávat oznámení při překročení prahových hodnot.
Pro diagnostiku Starvation způsobeného bloky synchronized použijte Java Flight Recorder (JFR) (dostupný na Androidu přes OpenJDK API) nebo Async Profiler. Tyto nástroje ukazují, které monitory mají nejdelší dobu čekání a která vlákna soutěží o každý monitor. Data z JFR se integrují s IntelliJ IDEA Ultimate prostřednictvím vestavěného profileru.
ReentrantLock(true) zaručuje, že vlákna dostávají zámek v pořadí fronty (FIFO). Na rozdíl od synchronized, fair lock neumožňuje situaci, kdy vlákno, které právě uvolnilo zámek, ho okamžitě znovu převezme. To zcela eliminuje Starvation, i když snižuje celkovou propustnost o 10-20 % kvůli režii na udržování fronty.
Lock-free datové struktury (ConcurrentHashMap, AtomicReference, LongAdder) ze své podstaty vylučují Starvation, protože neobsahují zámky, které by mohlo držet jedno vlákno. Všechny operace používají instrukce CAS procesoru, které zaručují pokrok alespoň jednoho vlákna v konečném počtu kroků. Pro mobilní vývoj preferujte ConcurrentLinkedQueue pro fronty úkolů.
Minimalizace doby držení zámku — univerzální způsob snížení rizika Starvation. Vyneste těžké operace (síť, disk I/O, složité výpočty) mimo blok synchronized. Použijte ReadWriteLock pro scénáře, kde by čtenáři neměli hladovět kvůli vzácným zapisovatelům. Knihovna Kotlin Coroutines poskytuje Mutex s mechanismem pozastavení (suspending), který neblokuje vlákno OS.
Condition.await() a signal() by měly být používány opatrně: vlákno čekající na Condition se probouzí spolu s dalšími vlákny (spurious wakeup) a všechna soutěží o zámek. Pokud se jedno vlákno po await okamžitě vrátí k čekání, zatímco jiná stihnou zámek převzít — hladovějící vlákno se může probouzet a usínat donekonečna. Vždy kontrolujte podmínku ve smyčce while, ne v if, abyste zaručili opětovnou kontrolu.
Často kladené otázky
Priority Inversion — je situace, kdy vlákno s nízkou prioritou drží zámek potřebný pro vlákno s vysokou prioritou. V důsledku toho vlákno s vysokou prioritou čeká na vlákno s nízkou prioritou — priority se obracejí. Starvation je širší problém: vlákno nezíská prostředek bez ohledu na prioritu, kvůli nespravedlivému plánování nebo dlouhým kritickým sekcím.
Ne, Starvation — je problém vícevláknového programování. V jednovláknovém kódu neexistuje konkurence o prostředky a plánování vláken. Starvation však může vzniknout v asynchronním jednovláknovém kódu (např. JavaScript event loop), pokud jedna mikroúloha donekonečna odkládá provedení jiných pomocí setTimeout s nulovým zpožděním.
JMM (Java Memory Model) definuje pravidla viditelnosti změn mezi vlákny, ale nezaručuje spravedlivé plánování. synchronized v souladu s JMM poskytuje sekvenční konzistenci — základní správnost — ale nezabraňuje Starvation. Pro spravedlnost jsou zapotřebí další mechanismy, které nejsou součástí specifikace JMM.
UI vlákno (Main Thread) nemůže hladovět v klasickém smyslu, protože má nejvyšší prioritu. Starvation však nastává, když vlákno UI čeká na výsledek hladovějícího vlákna na pozadí. Typický scénář: AsyncTask nebo korutina načítá data, ale nemůže získat přístup k databázi kvůli konkurenci s jinými vlákny, a UI zamrzne v očekávání.
V korutinách pro prevenci Starvation používejte limitedParallelism na Dispatchers.IO, abyste předešli vyčerpání vláken. Pro synchronizaci aplikujte Mutex z kotlinx.coroutines.sync — pozastavuje korutinu, neblokuje vlákno, což snižuje riziko hladovění. Vyhněte se runBlocking v korutinách, protože může zachytit vlákno z poolu a způsobit Starvation jiných korutin.
Shrnutí
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í.
Přečtěte si také