Starvation (înfometarea thread-ului) — este situația în care un thread nu obține acces la resursa necesară pentru continuarea lucrului, deși este pregătit pentru execuție. Conform Baeldung (Java Thread Starvation, 2024), înfometarea apare din cauza planificării inechitabile, când thread-urile cu prioritate scăzută sunt amânate constant în favoarea celor cu prioritate mai mare. Spre deosebire de Deadlock, Starvation nu blochează thread-ul — acesta rămâne în starea RUNNABLE, dar nu primește niciodată timp de procesor.
Principalele puncte
Starvation (înfometarea thread-ului) — este o problemă a programării multi-thread în care un thread nu poate obține acces la resursa necesară pentru îndeplinirea sarcinii, deși resursa nu este blocată pentru totdeauna de un alt thread. Thread-ul se află în starea RUNNABLE, dar planificatorul sau mecanismul de sincronizare amână sistematic execuția lui în favoarea altor thread-uri.
În dezvoltarea mobilă, Starvation se manifestă ca execuție inegală a sarcinilor: unele operații se execută instantaneu, altele — cu întârzieri catastrofale. De exemplu, thread-ul de fundal care sincronizează datele poate să nu obțină niciodată acces la baza de date dacă thread-ul UI și handler-ele de animație îl depășesc constant. Potrivit Android Developer Blog (Performance Matters, 2023), aproximativ 12% din cazurile de cadre pierdute (jank) pe Android sunt cauzate de Starvation a sarcinilor de fundal de care depinde randarea.
Diferența cheie dintre Starvation și Deadlock — reversibilitatea. Dacă sarcina sistemului scade sau prioritățile sunt redistribuite, thread-ul înfometat poate obține resursa și finaliza lucrul. Cu toate acestea, în condiții de sarcină ridicată constantă, Starvation poate dura nelimitat, creând impresia unei aplicații înghețate.
synchronized în Java și Kotlin — exemplul clasic de mecanism inechitabil. La concurență ridicată, JVM poate acorda blocarea la nesfârșit acelorași thread-uri active, în timp ce alte thread-uri pierd constant în cursă. Aceasta nu este o eroare a JVM, ci o caracteristică a implementării: blocările inechitabile asigură un randament mai mare în detrimentul uniformității accesului. Pentru aplicațiile mobile cu 4-8 thread-uri, această problemă este deosebit de relevantă.
Setarea priorităților diferite ale thread-urilor poate duce la Starvation a thread-urilor cu prioritate scăzută. În Android Runtime, planificatorul CFS (Completely Fair Scheduler) al Linux-ului distribuie timpul procesorului proporțional cu prioritățile, iar dacă thread-urile cu prioritate ridicată sunt constant active, cele cu prioritate scăzută pot să nu primească niciodată CPU. Google recomandă categoric să nu modificați prioritățile thread-urilor în Android — sistemul le gestionează singur.
Dacă un thread menține blocarea prea mult timp (execută calcule grele, cereri de rețea sau operații cu fișiere în interiorul blocului synchronized), alte thread-uri care așteaptă această blocare înfometează. Acest lucru este deosebit de periculos în Android, unde operațiile lungi în thread-ul UI cauzează ANR, iar transferul lor în thread-uri de fundal fără optimizarea secțiunilor critice transferă problema Starvation către thread-urile de lucru.
Să considerăm un exemplu în care un thread preia blocarea prea des din cauza planificării inechitabile. Starvation este demonstrată printr-o buclă infinită a thread-ului cu prioritate ridicată, care nu permite thread-ului cu prioritate scăzută să obțină acces la resursa comună.
class SharedResource {
private val lock = Any()
fun criticalSection(id: String) {
synchronized(lock) {
println("$id a obținut acces")
Thread.sleep(10) // simularea lucrului
}
}
}
fun main() {
val resource = SharedResource()
// Thread cu prioritate ridicată — activ constant
val highPriority = Thread {
while (true) {
resource.criticalSection("High")
}
}
// Thread cu prioritate scăzută — poate să nu obțină niciodată acces
val lowPriority = Thread {
while (true) {
resource.criticalSection("Low")
}
}
highPriority.start()
lowPriority.start()
// „Low" poate să nu afișeze niciodată mesajul — Starvation!
}
În acest exemplu, thread-ul highPriority preia constant blocarea și o eliberează doar pentru 10 ms. Din cauza naturii inechitabile a lui synchronized, planificatorul JVM cu o probabilitate mare va acorda blocarea din nou aceluiași thread care tocmai a eliberat-o — thread-ul cu prioritate scăzută înfometează. Soluția — utilizarea ReentrantLock(true) cu indicatorul fair, care garantează ordinea în coada de așteptare.
Versiunea corectată cu fair lock asigură distribuirea echitabilă a accesului la resursă.
class FairSharedResource {
private val fairLock = ReentrantLock(true) // fair = true
fun criticalSection(id: String) {
fairLock.lock()
try {
println("$id a obținut acces (fair)")
Thread.sleep(10)
} finally {
fairLock.unlock()
}
}
}
Trei probleme clasice ale multi-threading-ului — Starvation, Deadlock și Livelock — sunt adesea combinate, dar mecanismele lor și modalitățile de eliminare sunt diferite. Starvation — thread-ul este pregătit, dar nu primește resursa. Deadlock — thread-urile sunt blocate de așteptarea ciclică. Livelock — thread-urile sunt active, dar nu progresează.
| Parametru | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Starea thread-ului | RUNNABLE | BLOCKED | RUNNABLE |
| Progres | Nu | Nu | Nu (deși activ) |
| Consum CPU | Scăzut | Minim | Ridicat (până la 100%) |
| Cauză | Planificare inechitabilă | Așteptare ciclică | Aceeași reacție la conflict |
| Soluția principală | Fair Lock, reducerea secțiunilor critice | Ierarhia blocărilor | Limită reîncercări, backoff exponențial |
Starvation este considerată mai puțin critică decât Deadlock, deoarece nu este fatală — la scăderea sarcinii, thread-ul înfometat se va executa în cele din urmă. Cu toate acestea, în condițiile de utilizare reală a aplicațiilor Android, unde memoria și CPU sunt limitate, Starvation poate dura minute întregi, creând o experiență de utilizare inacceptabilă.
Thread Dump cu prelevări repetate la intervale scurte — metoda de bază de detectare a înfometării. Dacă un thread se află constant în starea RUNNABLE, dar stiva sa de apeluri nu se modifică pe parcursul mai multor dump-uri — acesta este un semn clasic de Starvation. În Android Studio, pentru aceasta se utilizează Android Profiler cu înregistrarea stării thread-urilor în timp.
Detectarea automatizată este posibilă prin monitorizarea timpului de execuție a sarcinilor. Dacă o sarcină cu timp de execuție previzibil (de exemplu, 50 ms) se execută timp de 5 secunde sau mai mult — există o probabilitate ridicată de Starvation. În aplicațiile mobile, Firebase Performance Monitoring permite configurarea de trace-uri personalizate pentru secțiuni critice și primirea de notificări la depășirea valorilor prag.
Pentru diagnosticarea Starvation cauzată de blocurile synchronized, utilizați Java Flight Recorder (JFR) (disponibil pe Android prin OpenJDK API) sau Async Profiler. Aceste instrumente arată care monitoare au cel mai mare timp de așteptare și care thread-uri concurează pentru fiecare monitor. Datele JFR se integrează cu IntelliJ IDEA Ultimate prin profilatorul încorporat.
ReentrantLock(true) garantează că thread-urile primesc blocarea în ordinea cozii (FIFO). Spre deosebire de synchronized, fair lock nu permite situația în care thread-ul care tocmai a eliberat blocarea o preia imediat din nou. Aceasta elimină complet Starvation, deși reduce randamentul general cu 10-20% din cauza cheltuielilor suplimentare pentru menținerea cozii.
Structurile de date Lock-free (ConcurrentHashMap, AtomicReference, LongAdder) elimină Starvation prin definiție, deoarece nu conțin blocări care pot fi menținute de un singur thread. Toate operațiile utilizează instrucțiunile CAS ale procesorului, care garantează progresul a cel puțin unui thread într-un număr finit de pași. Pentru dezvoltarea mobilă, preferați ConcurrentLinkedQueue pentru cozile de sarcini.
Minimizarea timpului de menținere a blocării — o modalitate universală de a reduce riscul de Starvation. Scoateți operațiile grele (rețea, intrare-ieșire pe disc, calcule complexe) în afara blocului synchronized. Utilizați ReadWriteLock pentru scenarii în care cititorii nu ar trebui să înfometeze din cauza scriitorilor rari. Biblioteca Kotlin Coroutines furnizează Mutex cu un mecanism de suspendare care nu blochează thread-ul sistemului de operare.
Condition.await() și signal() trebuie utilizate cu prudență: thread-ul care așteaptă pe Condition se trezește împreună cu alte thread-uri (spurious wakeup) și toate concurează pentru blocare. Dacă un thread după await revine imediat la așteptare, iar altele reușesc să preia blocarea — thread-ul înfometat se poate trezi și adormi la nesfârșit. Verificați întotdeauna condiția într-o buclă while, nu în if, pentru a garanta reverificarea.
Întrebări frecvente
Priority Inversion — este situația în care un thread cu prioritate scăzută menține o blocare necesară unui thread cu prioritate ridicată. Ca rezultat, thread-ul cu prioritate ridicată îl așteaptă pe cel cu prioritate scăzută — prioritățile se inversează. Starvation este o problemă mai largă: thread-ul nu primește resursa indiferent de prioritate, din cauza planificării inechitabile sau a secțiunilor critice lungi.
Nu, Starvation — este o problemă a multi-threading-ului. În codul monothread nu există concurență pentru resurse și planificare a thread-urilor. Cu toate acestea, Starvation poate apărea în codul asincron monothread (de exemplu, bucla de evenimente JavaScript), dacă o micro-sarcină amână la nesfârșit execuția altora prin setTimeout cu întârziere zero.
JMM (Java Memory Model) definește regulile de vizibilitate a modificărilor între thread-uri, dar nu garantează planificarea echitabilă. synchronized în conformitate cu JMM asigură consistenta secvențială — corectitudinea de bază — dar nu previne Starvation. Pentru echitate sunt necesare mecanisme suplimentare care nu fac parte din specificația JMM.
Thread-ul UI (Main Thread) nu poate înfometa în sensul clasic, deoarece are cea mai mare prioritate. Cu toate acestea, Starvation apare atunci când thread-ul UI așteaptă rezultatul unui thread de fundal înfometat. Scenariul tipic: AsyncTask sau corutina încarcă date, dar nu pot accesa baza de date din cauza concurenței cu alte thread-uri, iar UI îngheață în așteptare.
În corutine, pentru prevenirea Starvation, utilizați limitedParallelism pe Dispatchers.IO pentru a evita epuizarea thread-urilor. Pentru sincronizare, aplicați Mutex din kotlinx.coroutines.sync — acesta suspendă corutina, nu blochează thread-ul, ceea ce reduce riscul de înfometare. Evitați runBlocking în corutine, deoarece poate captura thread-ul din pool și poate cauza Starvation altor corutine.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și