Starvation (inedia del thread) è una situazione in cui un thread non può accedere a una risorsa necessaria per continuare il suo lavoro, sebbene sia pronto per l'esecuzione. Secondo Baeldung (Java Thread Starvation, 2024), l'inedia si verifica a causa di una schedulazione non equa, in cui i thread a bassa priorità vengono costantemente posticipati a favore di quelli a priorità più alta. A differenza di Deadlock, Starvation non blocca il thread — rimane nello stato RUNNABLE ma non riceve mai tempo CPU.
Punti Chiave
Starvation (inedia del thread) è un problema di programmazione multithread in cui un thread non può accedere a una risorsa necessaria per completare il suo compito, sebbene la risorsa non sia bloccata permanentemente da un altro thread. Il thread è nello stato RUNNABLE, ma lo scheduler o il meccanismo di sincronizzazione posticipa sistematicamente la sua esecuzione a favore di altri thread.
Nello sviluppo mobile, Starvation si manifesta come un'esecuzione diseguale dei compiti: alcune operazioni vengono eseguite istantaneamente, mentre altre subiscono ritardi catastrofici. Ad esempio, un thread di sincronizzazione dati in background potrebbe non ottenere mai l'accesso al database se il thread UI e i gestori di animazioni lo precedono costantemente. Secondo Android Developer Blog (Performance Matters, 2023), circa il 12% dei frame persi (jank) su Android sono causati da Starvation di attività in background da cui dipende il rendering.
La differenza chiave tra Starvation e Deadlock è la reversibilità. Se il carico del sistema diminuisce o le priorità vengono ridistribuite, il thread affamato può acquisire la risorsa e completare il suo lavoro. Tuttavia, sotto un carico elevato sostenuto, Starvation può durare indefinitamente, creando l'impressione di un'applicazione congelata.
synchronized in Java e Kotlin è un classico esempio di meccanismo non equo. Sotto alta contesa, la JVM può concedere continuamente il blocco agli stessi thread attivi, mentre altri thread perdono costantemente la gara. Questo non è un bug della JVM ma un compromesso di progettazione: i blocchi non equi forniscono una maggiore produttività a scapito dell'equità di accesso. Per le applicazioni mobili con 4–8 thread, questo problema è particolarmente rilevante.
Impostare priorità di thread diverse può portare a Starvation dei thread a bassa priorità. In Android Runtime, lo scheduler CFS (Completely Fair Scheduler) di Linux distribuisce il tempo CPU proporzionalmente alle priorità, e se i thread ad alta priorità sono costantemente attivi, i thread a bassa priorità potrebbero non ricevere mai tempo CPU. Google sconsiglia vivamente di modificare le priorità dei thread in Android — il sistema le gestisce da solo.
Se un thread mantiene un blocco per troppo tempo (eseguendo calcoli pesanti, richieste di rete o operazioni su file all'interno di un blocco synchronized), altri thread in attesa di quel blocco soffrono di inedia. Questo è particolarmente pericoloso in Android, dove le operazioni lunghe sul thread UI causano ANR, e spostarle in thread in background senza ottimizzare le sezioni critiche trasferisce semplicemente il problema di Starvation ai thread worker.
Considera un esempio in cui un thread acquisisce un blocco troppo frequentemente a causa di una schedulazione non equa. Starvation è dimostrata attraverso un ciclo infinito di un thread ad alta priorità che impedisce a un thread a bassa priorità di accedere a una risorsa condivisa.
class SharedResource {
private val lock = Any()
fun criticalSection(id: String) {
synchronized(lock) {
println("$id ha ottenuto accesso")
Thread.sleep(10) // simulazione di lavoro
}
}
}
fun main() {
val resource = SharedResource()
// Thread ad alta priorità — costantemente attivo
val highPriority = Thread {
while (true) {
resource.criticalSection("High")
}
}
// Thread a bassa priorità — potrebbe non ottenere mai accesso
val lowPriority = Thread {
while (true) {
resource.criticalSection("Low")
}
}
highPriority.start()
lowPriority.start()
// "Low" potrebbe non stampare mai un messaggio — Starvation!
}
In questo esempio, il thread highPriority acquisisce costantemente il blocco e lo rilascia solo per 10 ms. A causa della natura non equa di synchronized, lo scheduler JVM con ogni probabilità concederà il blocco di nuovo allo stesso thread che lo ha appena rilasciato — il thread a bassa priorità soffre di inedia. La soluzione è usare ReentrantLock(true) con il flag fair, che garantisce un ordine di attesa FIFO.
La versione corretta con un blocco equo assicura una distribuzione equa dell'accesso alla risorsa.
class FairSharedResource {
private val fairLock = ReentrantLock(true) // fair = true
fun criticalSection(id: String) {
fairLock.lock()
try {
println("$id ha ottenuto accesso (fair)")
Thread.sleep(10)
} finally {
fairLock.unlock()
}
}
}
Tre problemi classici del multithreading — Starvation, Deadlock e Livelock — sono spesso raggruppati, ma i loro meccanismi e soluzioni differiscono. Starvation — il thread è pronto ma non può acquisire la risorsa. Deadlock — i thread sono bloccati da attesa ciclica. Livelock — i thread sono attivi ma non progrediscono.
| Parametro | Starvation | Deadlock | Livelock |
|---|---|---|---|
| Stato del thread | RUNNABLE | BLOCKED | RUNNABLE |
| Progresso | Nessuno | Nessuno | Nessuno (sebbene attivo) |
| Utilizzo CPU | Basso | Minimo | Alto (fino al 100%) |
| Causa | Schedulazione non equa | Attesa ciclica | Stessa risposta al conflitto |
| Correzione principale | Fair Lock, accorciare sezioni critiche | Gerarchia di blocchi | Limite di tentativi, exponential backoff |
Starvation è considerato meno critico di Deadlock perché non è fatale — sotto carico ridotto, il thread affamato alla fine verrà eseguito. Tuttavia, nell'uso reale di Android, dove memoria e CPU sono limitati, Starvation può durare minuti, creando un'esperienza utente inaccettabile.
Thread Dump eseguito ripetutamente a brevi intervalli — il metodo di base per rilevare l'inedia. Se un thread è costantemente nello stato RUNNABLE ma il suo stack di chiamate non cambia attraverso più dump — questo è un segno classico di Starvation. In Android Studio, usa Android Profiler con registrazione dello stato dei thread nel tempo.
Il rilevamento automatizzato è possibile attraverso il monitoraggio del tempo di esecuzione delle attività. Se un'attività con un tempo di esecuzione prevedibile (ad esempio, 50 ms) impiega 5 secondi o più — c'è un'alta probabilità di Starvation. Nelle applicazioni mobili, Firebase Performance Monitoring consente di configurare trace personalizzate per sezioni critiche e ricevere notifiche quando le soglie vengono superate.
Per diagnosticare Starvation causata da blocchi synchronized, usa Java Flight Recorder (JFR) (disponibile su Android tramite l'API OpenJDK) o Async Profiler. Questi strumenti mostrano quali monitor hanno i tempi di attesa più alti e quali thread competono per ciascun monitor. I dati JFR si integrano con IntelliJ IDEA Ultimate tramite il suo profiler integrato.
ReentrantLock(true) garantisce che i thread acquisiscano il blocco in ordine FIFO. A differenza di synchronized, un blocco equo non permette a un thread che ha appena rilasciato il blocco di riacquisirlo immediatamente. Questo elimina completamente Starvation, sebbene riduca la produttività complessiva del 10–20% a causa dell'overhead di mantenimento della coda.
Strutture dati senza blocco (ConcurrentHashMap, AtomicReference, LongAdder) eliminano Starvation per definizione, poiché non hanno blocchi che possono essere trattenuti da un thread. Tutte le operazioni usano istruzioni CAS della CPU che garantiscono che almeno un thread progredisca in un numero finito di passi. Per lo sviluppo mobile, preferisci ConcurrentLinkedQueue per le code di attività.
Minimizzare il tempo di trattenuta del blocco è un modo universale per ridurre il rischio di Starvation. Sposta le operazioni pesanti (rete, I/O su disco, calcoli complessi) fuori dai blocchi synchronized. Usa ReadWriteLock per scenari in cui i lettori non dovrebbero soffrire di inedia a causa di scrittori infrequenti. La libreria Kotlin Coroutines fornisce Mutex con un meccanismo di sospensione che non blocca un thread del sistema operativo.
Condition.await() e signal() devono essere usati con cautela: un thread in attesa su una Condition si risveglia insieme ad altri thread (spurious wakeup), e tutti competono per il blocco. Se un thread torna immediatamente in attesa dopo await mentre altri riescono ad acquisire il blocco, il thread affamato può svegliarsi e riaddormentarsi indefinitamente. Controlla sempre la condizione in un ciclo while anziché in un if per garantire la riverifica.
Domande Frequenti
Priority Inversion è una situazione in cui un thread a bassa priorità trattiene un blocco necessario a un thread ad alta priorità. Di conseguenza, il thread ad alta priorità aspetta quello a bassa priorità — le priorità vengono invertite. Starvation è un problema più ampio: un thread non può acquisire una risorsa indipendentemente dalla priorità, a causa di schedulazione non equa o sezioni critiche lunghe.
No, Starvation è un problema di multithreading. Il codice single-thread non ha contesa di risorse o schedulazione di thread. Tuttavia, Starvation può verificarsi in codice asincrono single-thread (ad esempio, il ciclo di eventi JavaScript) se una microattività posticipa indefinitamente l'esecuzione di altre tramite setTimeout con ritardo zero.
JMM (Java Memory Model) definisce le regole per la visibilità dei cambiamenti tra thread ma non garantisce una schedulazione equa. synchronized, secondo JMM, assicura coerenza sequenziale — correttezza di base — ma non previene Starvation. L'equità richiede meccanismi aggiuntivi non specificati in JMM.
Il thread UI (Main Thread) non può soffrire di inedia nel senso classico perché ha la priorità più alta. Tuttavia, Starvation si verifica quando il thread UI attende un risultato da un thread in background affamato. Uno scenario tipico: un AsyncTask o coroutine carica dati ma non può accedere al database a causa della contesa con altri thread, e l'UI si blocca in attesa.
Nelle coroutine, per prevenire Starvation, usa limitedParallelism su Dispatchers.IO per evitare l'esaurimento dei thread. Per la sincronizzazione, usa Mutex da kotlinx.coroutines.sync — sospende la coroutine anziché bloccare il thread, riducendo il rischio di inedia. Evita runBlocking nelle coroutine, poiché può catturare un thread del pool e causare Starvation di altre coroutine.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche