Starvation nelle applicazioni mobili — essenza, cause e metodi per prevenire l'inedia del thread

Autore: IT Sectr Pubblicato: 2026-03-18 Tempo di lettura: 10 min

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 — situazione in cui un thread non può accedere a una risorsa nonostante sia pronto per l'esecuzione
  • A differenza di Deadlock, un thread in inedia rimane nello stato RUNNABLE — non è bloccato, ma non progredisce
  • Schedulazione non equa (ad esempio, sincronizzazione tramite synchronized) — la causa principale di Starvation sulla JVM
  • Fair Lock (ReentrantLock(true)) garantisce un ordine FIFO equo di acquisizione del blocco
  • Thread Priority nello sviluppo mobile si raccomanda di non modificarla — Android Runtime gestisce le priorità da sola

Cos'è Starvation?

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.

Cause dell'inedia del thread

Blocchi non equi (Non-Fair Locks)

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.

Uso improprio delle priorità

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.

Sezioni critiche lunghe

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.

Esempio di Starvation in codice Kotlin

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.

kotlin
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.

kotlin
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()
        }
    }
}

Starvation vs Deadlock vs Livelock

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.

ParametroStarvationDeadlockLivelock
Stato del threadRUNNABLEBLOCKEDRUNNABLE
ProgressoNessunoNessunoNessuno (sebbene attivo)
Utilizzo CPUBassoMinimoAlto (fino al 100%)
CausaSchedulazione non equaAttesa ciclicaStessa risposta al conflitto
Correzione principaleFair Lock, accorciare sezioni criticheGerarchia di blocchiLimite 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.

Come rilevare Starvation

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.

Metodi per prevenire l'inedia del thread

Fair Lock (ReentrantLock con flag true)

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 atomiche senza blocco

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à.

Sezioni critiche brevi

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.

Variabili di condizione e segnali

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

Qual è la differenza tra Starvation e inversione di priorità (Priority Inversion)?

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.

Starvation può verificarsi in un'applicazione single-thread?

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.

Come si relaziona il Modello di Memoria Java con Starvation?

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.

Cos'è Starvation nel thread UI di Android?

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.

Come prevenire Starvation in Kotlin Coroutines?

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

  • Starvation — situazione in cui un thread è pronto per l'esecuzione ma non può acquisire una risorsa a causa di schedulazione non equa
  • A differenza di Deadlock, un thread affamato rimane nello stato RUNNABLE e può essere eseguito quando il carico diminuisce
  • Blocchi non equi (synchronized) e uso improprio delle priorità sono le principali cause di Starvation
  • Fair Lock (ReentrantLock con flag true) garantisce ordine di accesso FIFO ed elimina completamente l'inedia
  • Strutture senza blocco (ConcurrentHashMap, AtomicReference) eliminano Starvation a livello architetturale
  • Thread Dump con catture ripetute e Java Flight Recorder sono metodi efficaci per diagnosticare Starvation
  • Sezioni critiche brevi e ReadWriteLock riducono la probabilità di inedia in sistemi ad alto carico

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.

Discuti il progetto

Leggi anche