Starvation (thread-uithongering) — is de situatie waarin een thread geen toegang krijgt tot de resource die nodig is om verder te werken, hoewel deze zelf klaar is voor uitvoering. Volgens Baeldung (Java Thread Starvation, 2024) ontstaat uithongering door oneerlijke planning, wanneer threads met een lage prioriteit constant worden uitgesteld ten gunste van threads met een hogere prioriteit. In tegenstelling tot Deadlock, Starvation blokkeert de thread niet — deze blijft in de RUNNABLE-status, maar krijgt nooit processortijd.
Belangrijkste punten
Starvation (thread-uithongering) — is een probleem van multi-thread programmeren waarbij een thread geen toegang kan krijgen tot de resource die nodig is om een taak uit te voeren, hoewel de resource niet permanent is geblokkeerd door een andere thread. De thread bevindt zich in de RUNNABLE-status, maar de planner of het synchronisatiemechanisme stelt de uitvoering ervan systematisch uit ten gunste van andere threads.
In mobiele ontwikkeling manifesteert Starvation zich als ongelijke taakuitvoering: sommige bewerkingen worden onmiddellijk uitgevoerd, andere — met catastrofale vertragingen. Een achtergrondthread die gegevens synchroniseert, krijgt bijvoorbeeld nooit toegang tot de database als de UI-thread en animatie-handlers deze constant voorblijven. Volgens Android Developer Blog (Performance Matters, 2023) wordt ongeveer 12% van de gemiste frames (jank) op Android veroorzaakt door Starvation van achtergrondtaken waarvan de weergave afhankelijk is.
Het belangrijkste verschil tussen Starvation en Deadlock — omkeerbaarheid. Als de systeembelasting afneemt of prioriteiten worden herverdeeld, kan de uitgehongerde thread de resource krijgen en het werk voltooien. Echter, onder constante hoge belasting kan Starvation onbepaald lang duren, waardoor de indruk van een vastgelopen applicatie ontstaat.
synchronized in Java en Kotlin — een klassiek voorbeeld van een oneerlijk mechanisme. Bij hoge concurrentie kan JVM de vergrendeling oneindig aan dezelfde actieve threads geven, terwijl andere threads constant de race verliezen. Dit is geen bug in JVM, maar een implementatiekenmerk: oneerlijke vergrendelingen zorgen voor een hogere doorvoer ten koste van gelijkmatige toegang. Voor mobiele applicaties met 4-8 threads is dit probleem bijzonder relevant.
Het instellen van verschillende thread-prioriteiten kan leiden tot Starvation van threads met lage prioriteit. In Android Runtime verdeelt de CFS (Completely Fair Scheduler) planner van Linux de processortijd evenredig aan de prioriteiten, en als threads met hoge prioriteit constant actief zijn, kunnen threads met lage prioriteit nooit CPU krijgen. Google raadt ten zeerste af om thread-prioriteiten in Android te wijzigen — het systeem beheert ze zelf.
Als een thread de vergrendeling te lang vasthoudt (zware berekeningen, netwerkverzoeken of bestandsbewerkingen uitvoert binnen een synchronized-blok), lijden andere threads die op deze vergrendeling wachten honger. Dit is vooral gevaarlijk in Android, waar lange bewerkingen in de UI-thread ANR veroorzaken, en het verplaatsen naar achtergrondthreads zonder optimalisatie van kritieke secties het Starvation-probleem overdraagt naar werkthreads.
Laten we een voorbeeld bekijken waarin een thread de vergrendeling te vaak opeet als gevolg van oneerlijke planning. Starvation wordt gedemonstreerd door een oneindige lus van een thread met hoge prioriteit, die een thread met lage prioriteit geen toegang geeft tot de gedeelde resource.
class SharedResource {
private val lock = Any()
fun criticalSection(id: String) {
synchronized(lock) {
println("$id kreeg toegang")
Thread.sleep(10) // simulatie van werk
}
}
}
fun main() {
val resource = SharedResource()
// Thread met hoge prioriteit — constant actief
val highPriority = Thread {
while (true) {
resource.criticalSection("High")
}
}
// Thread met lage prioriteit — krijgt mogelijk nooit toegang
val lowPriority = Thread {
while (true) {
resource.criticalSection("Low")
}
}
highPriority.start()
lowPriority.start()
// „Low" geeft mogelijk nooit een bericht weer — Starvation!
}
In dit voorbeeld pakt de highPriority-thread constant de vergrendeling en geeft deze slechts 10 ms vrij. Door de oneerlijke aard van synchronized zal de JVM-planner met grote waarschijnlijkheid de vergrendeling opnieuw aan dezelfde thread geven die deze net heeft vrijgegeven — de thread met lage prioriteit lijdt honger. De oplossing is het gebruik van ReentrantLock(true) met de fair-vlag, die de volgorde in de wachtrij garandeert.
De gecorrigeerde versie met fair lock zorgt voor een eerlijke verdeling van de toegang tot de resource.
class FairSharedResource {
private val fairLock = ReentrantLock(true) // fair = true
fun criticalSection(id: String) {
fairLock.lock()
try {
println("$id kreeg toegang (fair)")
Thread.sleep(10)
} finally {
fairLock.unlock()
}
}
}
Drie klassieke multi-threading problemen — Starvation, Deadlock en Livelock — worden vaak gecombineerd, maar hun mechanismen en oplossingsmethoden verschillen. Starvation — thread is gereed, maar krijgt geen resource. Deadlock — threads zijn geblokkeerd door cyclisch wachten. Livelock — threads zijn actief, maar maken geen vooruitgang.
| Parameter | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Thread-status | RUNNABLE | BLOCKED | RUNNABLE |
| Vooruitgang | Nee | Nee | Nee (hoewel actief) |
| CPU-verbruik | Laag | Minimaal | Hoog (tot 100%) |
| Oorzaak | Oneerlijke planning | Cyclisch wachten | Zelfde reactie op conflict |
| Belangrijkste oplossing | Fair Lock, verkorten kritieke secties | Vergrendelingshiërarchie | Pogingslimiet, exponentiële backoff |
Starvation wordt als minder kritiek beschouwd dan Deadlock, omdat het niet fataal is — bij verminderde belasting zal de uitgehongerde thread uiteindelijk worden uitgevoerd. Echter, onder reële gebruiksomstandigheden van Android-applicaties, waar geheugen en CPU beperkt zijn, kan Starvation minuten duren, wat een onaanvaardbare UX creëert.
Thread Dump met herhaalde momentopnamen op korte intervallen — de basismethode voor het detecteren van uithongering. Als een thread consistent in de RUNNABLE-status staat, maar zijn call-stack niet verandert gedurende meerdere dumps — is dit een klassiek teken van Starvation. In Android Studio wordt hiervoor Android Profiler gebruikt met registratie van thread-statussen in de tijd.
Geautomatiseerde detectie is mogelijk via monitoring van de uitvoeringstijd van taken. Als een taak met een voorspelbare uitvoeringstijd (bijv. 50 ms) 5 seconden of langer duurt — is er een grote kans op Starvation. In mobiele applicaties maakt Firebase Performance Monitoring het mogelijk om aangepaste traces te configureren voor kritieke secties en meldingen te ontvangen bij overschrijding van drempelwaarden.
Voor het diagnosticeren van Starvation door synchronized-blokken gebruikt u Java Flight Recorder (JFR) (beschikbaar op Android via OpenJDK API) of Async Profiler. Deze tools tonen welke monitors de langste wachttijd hebben en welke threads strijden om elke monitor. JFR-gegevens worden geïntegreerd met IntelliJ IDEA Ultimate via de ingebouwde profiler.
ReentrantLock(true) garandeert dat threads de vergrendeling ontvangen in wachtrijvolgorde (FIFO). In tegenstelling tot synchronized staat fair lock niet toe dat een thread die zojuist de vergrendeling heeft vrijgegeven, deze onmiddellijk opnieuw opeet. Dit elimineert Starvation volledig, hoewel het de algehele doorvoer met 10-20% vermindert vanwege de overhead voor het onderhouden van de wachtrij.
Lock-free datastructuren (ConcurrentHashMap, AtomicReference, LongAdder) sluiten Starvation per definitie uit, omdat ze geen vergrendelingen bevatten die door één thread kunnen worden vastgehouden. Alle bewerkingen gebruiken CAS-instructies van de processor, die vooruitgang van ten minste één thread in een eindig aantal stappen garanderen. Voor mobiele ontwikkeling geeft u de voorkeur aan ConcurrentLinkedQueue voor taakwachtrijen.
Minimalisatie van de tijd dat een vergrendeling wordt vastgehouden — een universele manier om het risico op Starvation te verminderen. Plaats zware bewerkingen (netwerk, schijf-I/O, complexe berekeningen) buiten het synchronized-blok. Gebruik ReadWriteLock voor scenario's waarin lezers niet mogen uithongeren door zeldzame schrijvers. De Kotlin Coroutines-bibliotheek biedt Mutex met een suspending-mechanisme dat de OS-thread niet blokkeert.
Condition.await() en signal() moeten voorzichtig worden gebruikt: een thread die op Condition wacht, wordt samen met andere threads gewekt (spurious wakeup) en ze concurreren allemaal om de vergrendeling. Als een thread na await onmiddellijk terugkeert naar wachten, terwijl andere erin slagen de vergrendeling te bemachtigen — kan de uitgehongerde thread eindeloos ontwaken en in slaap vallen. Controleer de conditie altijd in een while-lus, niet in een if, om hercontrole te garanderen.
Veelgestelde vragen
Priority Inversion — is de situatie waarin een thread met lage prioriteit een vergrendeling vasthoudt die nodig is voor een thread met hoge prioriteit. Als gevolg hiervan wacht de thread met hoge prioriteit op de thread met lage prioriteit — prioriteiten worden omgekeerd. Starvation is een breder probleem: de thread krijgt geen resource ongeacht prioriteit, als gevolg van oneerlijke planning of lange kritieke secties.
Nee, Starvation — is een multi-threading probleem. In single-thread code is er geen concurrentie om resources en thread-planning. Echter, Starvation kan optreden in asynchrone single-thread code (bijv. JavaScript event loop), als een microtaak de uitvoering van andere taken oneindig uitstelt via setTimeout met nulvertraging.
JMM (Java Memory Model) definieert de regels voor zichtbaarheid van wijzigingen tussen threads, maar garandeert geen eerlijke planning. synchronized in overeenstemming met JMM zorgt voor sequential consistency — basiscorrectheid — maar voorkomt Starvation niet. Voor eerlijkheid zijn aanvullende mechanismen nodig die geen deel uitmaken van de JMM-specificatie.
UI-thread (Main Thread) kan niet uithongeren in klassieke zin, omdat deze de hoogste prioriteit heeft. Echter, Starvation treedt op wanneer de UI-thread wacht op het resultaat van een uitgehongerde achtergrondthread. Typisch scenario: AsyncTask of coroutine laden gegevens, maar krijgen geen toegang tot de database door concurrentie met andere threads, en de UI bevriest in afwachting.
Gebruik in coroutines voor het voorkomen van Starvation limitedParallelism op Dispatchers.IO om uitputting van threads te voorkomen. Voor synchronisatie past u Mutex van kotlinx.coroutines.sync toe — dit onderbreekt de coroutine, blokkeert de thread niet, wat het risico op uithongering vermindert. Vermijd runBlocking in coroutines, omdat dit een pool-thread kan vastleggen en Starvation van andere coroutines kan veroorzaken.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook