Starvation mobil alkalmazásokban — lényeg, kialakulásának okai és a szál éhezésének megelőzési módszerei

Szerző: IT Sectr Megjelenés: 2026-03-18 Olvasási idő: 10 perc

Starvation (szál éheztetése) — az a helyzet, amikor egy szál nem fér hozzá a munka folytatásához szükséges erőforráshoz, bár maga készen áll a végrehajtásra. A Baeldung (Java Thread Starvation, 2024) szerint az éhezés tisztességtelen ütemezés miatt alakul ki, amikor az alacsony prioritású szálakat folyamatosan elhalasztják a magasabb prioritásúak javára. A Deadlock-tól eltérően a Starvation nem blokkolja a szálat — az RUNNABLE állapotban marad, de soha nem kap processzoridőt.

Főbb pontok

  • Starvation — olyan helyzet, amikor egy szál nem fér hozzá az erőforráshoz, a végrehajtásra való készenlét ellenére
  • A Deadlock-tól eltérően, az éhezés során a szál RUNNABLE állapotban marad — nincs blokkolva, de nem halad előre
  • Tisztességtelen ütemezés (pl. szinkronizáció synchronized segítségével) — a Starvation fő oka JVM-en
  • Fair Lock (ReentrantLock(true)) igazságos hozzáférési sorrendet garantál a zárhoz sorrendben
  • Thread Priority mobilfejlesztésben nem ajánlott megváltoztatni — az Android Runtime maga kezeli a prioritásokat

Mi az a Starvation?

Starvation (szál éheztetése) — a több szálas programozás azon problémája, amikor egy szál nem fér hozzá a feladat elvégzéséhez szükséges erőforráshoz, bár az erőforrást nem blokkolta örökre egy másik szál. A szál RUNNABLE állapotban van, de az ütemező vagy a szinkronizációs mechanizmus szisztematikusan elhalasztja a végrehajtását más szálak javára.

Mobilfejlesztésben a Starvation egyenlőtlen feladat-végrehajtásként nyilvánul meg: egyes műveletek azonnal végrehajtódnak, mások — katasztrofális késleltetéssel. Például egy adatokat szinkronizáló háttérszál soha nem férhet hozzá az adatbázishoz, ha az UI szál és az animációkezelők folyamatosan megelőzik. Az Android Developer Blog (Performance Matters, 2023) szerint az Androidon kihagyott keretek (jank) körülbelül 12%-át a megjelenítéstől függő háttérfeladatok Starvation-ja okozza.

A Starvation kulcsfontosságú különbsége a Deadlock-tól — visszafordíthatóság. Ha a rendszer terhelése csökken vagy a prioritások újraelosztásra kerülnek, az éhező szál hozzájuthat az erőforráshoz és befejezheti a munkát. Állandó magas terhelés mellett azonban a Starvation határozatlan ideig tarthat, befagyott alkalmazás benyomását keltve.

A szál éhezésének okai

Tisztességtelen zárak (Non-Fair Locks)

A synchronized Java-ban és Kotlin-ban — a tisztességtelen mechanizmus klasszikus példája. Magas versengés esetén a JVM végtelenül adhatja a zárat ugyanazoknak az aktív szálaknak, miközben más szálak folyamatosan veszítenek a versenyfutásban. Ez nem a JVM hibája, hanem a megvalósítás jellemzője: a tisztességtelen zárak magasabb áteresztőképességet biztosítanak a hozzáférés egyenletességének rovására. A 4-8 szálas mobilalkalmazások számára ez a probléma különösen releváns.

A prioritások helytelen használata

Különböző szálprioritások beállítása az alacsony prioritású szálak Starvation-jához vezethet. Az Android Runtime-ban a Linux CFS (Completely Fair Scheduler) ütemezője a processzoridőt a prioritásokkal arányosan osztja el, és ha a magas prioritású szálak folyamatosan aktívak, az alacsony prioritásúak soha nem kaphatnak CPU-t. A Google kategorikusan nem ajánlja a szálprioritások megváltoztatását Androidon — a rendszer maga kezeli azokat.

Hosszú kritikus szakaszok

Ha egy szál túl sokáig tartja a zárat (nehéz számításokat, hálózati kéréseket vagy fájlműveleteket hajt végre a synchronized blokkon belül), más, erre a zárra várakozó szálak éheznek. Ez különösen veszélyes Androidon, ahol a hosszú műveletek az UI szálban ANR-t okoznak, és ezek háttérszálakba helyezése a kritikus szakaszok optimalizálása nélkül a Starvation problémát a munkaszálakra helyezi át.

Starvation példa Kotlin kódban

Tekintsünk egy példát, ahol egy szál a tisztességtelen ütemezés miatt túl gyakran szerzi meg a zárat. A Starvation egy magas prioritású szál végtelen ciklusán keresztül kerül bemutatásra, amely nem engedi az alacsony prioritású szálnak, hogy hozzáférjen a megosztott erőforráshoz.

kotlin
class SharedResource {
    private val lock = Any()

    fun criticalSection(id: String) {
        synchronized(lock) {
            println("$id hozzáférést kapott")
            Thread.sleep(10)  // munka szimulációja
        }
    }
}

fun main() {
    val resource = SharedResource()

    // Magas prioritású szál — folyamatosan aktív
    val highPriority = Thread {
        while (true) {
            resource.criticalSection("High")
        }
    }

    // Alacsony prioritású szál — lehet, hogy soha nem fér hozzá
    val lowPriority = Thread {
        while (true) {
            resource.criticalSection("Low")
        }
    }

    highPriority.start()
    lowPriority.start()
    // „Low" lehet, hogy soha nem ír ki üzenetet — Starvation!
}

Ebben a példában a highPriority szál folyamatosan megszerzi a zárat, és csak 10 ms-re engedi el. A synchronized tisztességtelen természete miatt a JVM ütemező nagy valószínűséggel ismét ugyanannak a szálnak adja a zárat, amely épp most engedte el — az alacsony prioritású szál éhezik. A megoldás — a ReentrantLock(true) használata fair jelzővel, amely garantálja a sorrendet a várakozási sorban.

A fair lock-kal javított verzió biztosítja az erőforráshoz való hozzáférés igazságos elosztását.

kotlin
class FairSharedResource {
    private val fairLock = ReentrantLock(true)  // fair = true

    fun criticalSection(id: String) {
        fairLock.lock()
        try {
            println("$id hozzáférést kapott (fair)")
            Thread.sleep(10)
        } finally {
            fairLock.unlock()
        }
    }
}

Starvation vs Deadlock vs Livelock

A több szálas programozás három klasszikus problémája — Starvation, Deadlock és Livelock — gyakran összekapcsolódik, de mechanizmusaik és megszüntetésük módjai eltérőek. Starvation — a szál készen áll, de nem kap erőforrást. Deadlock — a szálak ciklikus várakozással blokkolva. Livelock — a szálak aktívak, de nem haladnak előre.

ParaméterStarvationDeadlockLivelock
Szál állapotaRUNNABLEBLOCKEDRUNNABLE
ElőrehaladásNemNemNem (bár aktív)
CPU fogyasztásAlacsonyMinimálisMagas (akár 100%)
OkTisztességtelen ütemezésCiklikus várakozásAzonos reakció konfliktusra
Fő megoldásFair Lock, kritikus szakaszok csökkentéseZárak hierarchiájaÚjrapróbálkozási korlát, exponenciális backoff

A Starvation kevésbé kritikusnak tekinthető, mint a Deadlock, mert nem végzetes — a terhelés csökkenésével az éhező szál végül végrehajtódik. Az Android alkalmazások valós használati körülményei között azonban, ahol a memória és a CPU korlátozott, a Starvation percekig tarthat, elfogadhatatlan UX-et teremtve.

Hogyan észleljük a Starvation-t

Thread Dump ismételt rögzítésekkel rövid időközönként — az éhezés észlelésének alapvető módszere. Ha egy szál következetesen RUNNABLE állapotban van, de a hívási verme nem változik több dump során — ez a Starvation klasszikus jele. Az Android Studio-ban erre az Android Profiler szolgál a szálak állapotának időbeli rögzítésével.

Automatizált észlelés lehetséges a feladatok végrehajtási idejének monitorozásával. Ha egy kiszámítható végrehajtási idejű feladat (pl. 50 ms) 5 másodpercig vagy tovább tart — nagy a Starvation valószínűsége. Mobilalkalmazásokban a Firebase Performance Monitoring lehetővé teszi egyéni nyomok (custom traces) konfigurálását kritikus szakaszokhoz és értesítések fogadását küszöbértékek túllépése esetén.

A synchronized blokkok által okozott Starvation diagnosztizálásához használja a Java Flight Recorder (JFR)-t (Androidon elérhető OpenJDK API-n keresztül) vagy az Async Profiler-t. Ezek az eszközök megmutatják, hogy mely monitorok rendelkeznek a leghosszabb várakozási idővel és mely szálak versengenek az egyes monitorokért. A JFR adatok integrálódnak az IntelliJ IDEA Ultimate-tel a beépített profileren keresztül.

A szál éhezésének megelőzési módszerei

Fair Lock (ReentrantLock true jelzővel)

ReentrantLock(true) garantálja, hogy a szálak a zárat sorrendben (FIFO) kapják meg. A synchronized-tól eltérően a fair lock nem engedi meg, hogy a zárat éppen elengedő szál azonnal újra megszerezze azt. Ez teljesen megszünteti a Starvation-t, bár a teljes áteresztőképességet 10-20%-kal csökkenti a sor karbantartásának többletköltségei miatt.

Zár nélküli atomi struktúrák

Lock-free adatstruktúrák (ConcurrentHashMap, AtomicReference, LongAdder) definíció szerint kizárják a Starvation-t, mivel nem tartalmaznak egy szál által tartható zárakat. Minden művelet a processzor CAS utasításait használja, amelyek garantálják legalább egy szál előrehaladását véges számú lépésben. Mobilfejlesztéshez előnyben részesítse a ConcurrentLinkedQueue-t a feladat sorokhoz.

Rövid kritikus szakaszok

A zár tartási idejének minimalizálása — univerzális módja a Starvation kockázatának csökkentésére. Helyezze a nehéz műveleteket (hálózat, lemez I/O, összetett számítások) a synchronized blokkon kívülre. Használjon ReadWriteLock-ot olyan forgatókönyvekhez, ahol az olvasók nem éhezhetnek a ritka írók miatt. A Kotlin Coroutines könyvtár Mutex-et biztosít suspending mechanizmussal, amely nem blokkolja az OS szálat.

Feltételes változók és jelek

Condition.await() és signal() óvatosan használandó: a Condition-re várakozó szál más szálakkal együtt ébred (spurious wakeup), és mindegyik verseng a zárért. Ha egy szál az await után azonnal visszatér a várakozáshoz, míg másoknak sikerül megszerezni a zárat — az éhező szál végtelenül ébredhet és alhat el. A feltételt mindig while ciklusban ellenőrizze, nem if-ben, az újbóli ellenőrzés garantálásához.

Gyakran ismételt kérdések

Mi a különbség a Starvation és a Priority Inversion között?

Priority Inversion — az a helyzet, amikor egy alacsony prioritású szál olyan zárat tart, amelyre egy magas prioritású szálnak van szüksége. Ennek eredményeként a magas prioritású szál vár az alacsony prioritásúra — a prioritások megfordulnak. A Starvation tágabb probléma: a szál prioritástól függetlenül nem kap erőforrást, tisztességtelen ütemezés vagy hosszú kritikus szakaszok miatt.

Előfordulhat-e Starvation egy szálú alkalmazásban?

Nem, a Starvation — több szálas probléma. Az egy szálú kódban nincs versengés erőforrásokért és szálütemezés. A Starvation azonban előfordulhat aszinkron egy szálú kódban (pl. JavaScript event loop), ha egy mikrofeladat végtelenül halasztja mások végrehajtását setTimeout segítségével nulla késleltetéssel.

Hogyan kapcsolódik a Java Memory Model a Starvation-hoz?

JMM (Java Memory Model) meghatározza a változtatások láthatóságának szabályait a szálak között, de nem garantálja a tisztességes ütemezést. A synchronized a JMM-nek megfelelően szekvenciális konzisztenciát — alapvető helyességet — biztosít, de nem akadályozza meg a Starvation-t. A tisztességességhez további, a JMM specifikáción kívüli mechanizmusok szükségesek.

Mi az a Starvation az Android UI szálban?

UI szál (Main Thread) klasszikus értelemben nem éhezhet, mivel a legmagasabb prioritással rendelkezik. A Starvation azonban akkor következik be, amikor az UI szál egy éhező háttérszál eredményére vár. Tipikus forgatókönyv: AsyncTask vagy korutin adatokat tölt be, de nem fér hozzá az adatbázishoz más szálakkal való versengés miatt, és az UI befagy a várakozásban.

Hogyan előzzük meg a Starvation-t Kotlin Coroutines-ben?

A korutinokban a Starvation megelőzéséhez használja a limitedParallelism-et a Dispatchers.IO-n a szálak kimerülésének elkerülésére. Szinkronizációhoz alkalmazza a Mutex-et a kotlinx.coroutines.sync-ből — ez felfüggeszti a korutint, nem blokkolja a szálat, ami csökkenti az éhezés kockázatát. Kerülje a runBlocking használatát korutinokban, mivel lefoglalhat egy pool szálat és más korutinok Starvation-ját okozhatja.

Összefoglalás

  • Starvation — olyan helyzet, amikor a szál készen áll a végrehajtásra, de tisztességtelen ütemezés miatt nem kap erőforrást
  • A Deadlock-tól eltérően, éhezéskor a szál RUNNABLE állapotban van és terhelés csökkenésekor végrehajtható
  • Tisztességtelen zárak (synchronized) és a prioritások helytelen használata — a Starvation fő okai
  • Fair Lock (ReentrantLock true jelzővel) FIFO hozzáférési sorrendet garantál és teljesen megszünteti az éhezést
  • Lock-free struktúrák (ConcurrentHashMap, AtomicReference) architektúra szinten küszöbölik ki a Starvation-t
  • Thread Dump ismételt rögzítésekkel és Java Flight Recorder — hatékony Starvation diagnosztikai módszerek
  • Rövid kritikus szakaszok és ReadWriteLock csökkentik az éhezés valószínűségét magas terhelésű rendszerekben

Kulcsrakész mobilalkalmazást fejlesztünk

Az IT Sectr 2017 óta készít iOS és Android alkalmazásokat induló vállalkozásoknak és vállalkozásoknak. Tanácsot adunk, és a legjobb megoldást javasoljuk.

Projekt megbeszélése

Olvassa el is