Starvation v mobilních aplikacích — podstata, příčiny vzniku a způsoby prevence hladovění vlákna

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

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 — situace, kdy vlákno nezíská přístup k prostředku, přestože je připraveno k vykonání
  • Na rozdíl od Deadlock, vlákno při hladovění zůstává ve stavu RUNNABLE — není blokováno, ale nepostupuje
  • Nespravedlivé plánování (např. synchronizace přes synchronized) — hlavní příčina Starvation na JVM
  • Fair Lock (ReentrantLock(true)) zaručuje spravedlivé pořadí přístupu k zámku v pořadí fronty
  • Thread Priority se v mobilním vývoji doporučuje neměnit — Android Runtime řídí priority sám

Co je Starvation?

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.

Příčiny hladovění vlákna

Nespravedlivé zámky (Non-Fair Locks)

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í.

Nesprávné použití priorit

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.

Dlouhé kritické sekce

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.

Příklad Starvation v kódu Kotlin

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.

kotlin
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.

kotlin
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()
        }
    }
}

Starvation vs Deadlock vs Livelock

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í.

ParametrStarvationDeadlockLivelock
Stav vláknaRUNNABLEBLOCKEDRUNNABLE
PokrokNeNeNe (i když aktivní)
Spotřeba CPUNízkáMinimálníVysoká (až 100%)
PříčinaNespravedlivé 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.

Jak odhalit Starvation

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.

Metody prevence hladovění vlákna

Fair Lock (ReentrantLock s příznakem true)

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.

Atomické struktury bez zámků

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ů.

Krátké kritické sekce

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.

Podmínkové proměnné a signály

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

Jaký je rozdíl mezi Starvation a inverzí priority (Priority Inversion)?

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.

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

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.

Jak souvisí Java Memory Model se Starvation?

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.

Co je Starvation ve vlákně UI v Androidu?

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í.

Jak zabránit Starvation v Kotlin Coroutines?

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í

  • Starvation — situace, kdy je vlákno připraveno k provedení, ale nezíská prostředek kvůli nespravedlivému plánování
  • Na rozdíl od Deadlock, při hladovění je vlákno ve stavu RUNNABLE a může se provést při snížení zátěže
  • Nespravedlivé zámky (synchronized) a nesprávné použití priorit — hlavní příčiny Starvation
  • Fair Lock (ReentrantLock s příznakem true) zaručuje pořadí přístupu FIFO a zcela eliminuje hladovění
  • Lock-free struktury (ConcurrentHashMap, AtomicReference) eliminují Starvation na úrovni architektury
  • Thread Dump s opakovanými snímky a Java Flight Recorder — efektivní metody diagnostiky Starvation
  • Krátké kritické sekce a ReadWriteLock snižují pravděpodobnost hladovění v systémech s vysokým zatížením

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é