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 (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 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.
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.
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.
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.
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.
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()
}
}
}
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éter | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Szál állapota | RUNNABLE | BLOCKED | RUNNABLE |
| Előrehaladás | Nem | Nem | Nem (bár aktív) |
| CPU fogyasztás | Alacsony | Minimális | Magas (akár 100%) |
| Ok | Tisztességtelen ütemezés | Ciklikus várakozás | Azonos reakció konfliktusra |
| Fő megoldás | Fair Lock, kritikus szakaszok csökkentése | Zá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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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
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.
Olvassa el is