Deadlock (blocco reciproco) è uno stato in cui due o più thread attendono indefinitamente il rilascio di risorse trattenute da altri partecipanti. Secondo Oracle Java Tutorials (2024), Deadlock si verifica in attesa circolare quando ogni thread detiene un blocco necessario a un altro thread. Senza strumenti di rilevamento speciali, Deadlock interrompe completamente l'esecuzione dell'applicazione senza errori visibili.
Punti chiave
Deadlock è una situazione nella programmazione multithread in cui due o più thread si bloccano permanentemente a vicenda. Ogni thread trattiene una risorsa necessaria a un altro thread e non la rilascia mentre attende di acquisire la risorsa mancante. Di conseguenza, nessuno dei thread può continuare l'esecuzione.
Nello sviluppo mobile, Deadlock è particolarmente critico perché non causa eccezioni o crash. L'applicazione smette semplicemente di rispondere alle azioni dell'utente (ANR — Application Not Responding), e l'unica via d'uscita è terminare forzatamente il processo. Secondo Google (Android Performance Patterns, 2023), circa il 15% dei rapporti ANR in Google Play Console sono correlati a blocchi reciproci nei thread in background.
La differenza chiave tra Deadlock e altri problemi di concorrenza è la sua irreversibilità senza intervento esterno. I thread non rilasceranno le risorse da soli perché lo scheduler del sistema operativo non può revocare forzatamente un blocco. Questo distingue Deadlock da Livelock, dove i thread sono attivi ma non svolgono lavoro utile.
Nel 1971, Edward G. Coffman formulò quattro condizioni obbligatorie necessarie per il verificarsi di Deadlock. Se almeno una di esse è assente, il blocco reciproco è impossibile. Queste condizioni sono note come condizioni di Coffman e costituiscono la base di tutti gli algoritmi di prevenzione di Deadlock.
Una risorsa può essere acquisita da un solo thread in ogni momento. Se una risorsa consente la lettura simultanea da più thread (ad esempio, ReadWriteLock in modalità lettura), Deadlock non si verifica. Questa condizione deriva dalla natura stessa di Mutex e dei blocchi.
Un thread trattiene una risorsa già acquisita e contemporaneamente attende di acquisire un'altra risorsa. Se un thread può rilasciare la risorsa corrente prima di richiedere la successiva (tramite blocco a due fasi), la condizione Hold and Wait viene infranta. In Android, ciò si manifesta spesso quando un thread trattiene un blocco del database e tenta di acquisire un blocco SharedPreferences.
Il sistema operativo non può togliere forzatamente un blocco a un thread. La risorsa viene rilasciata solo quando il thread stesso la rilascia. In alcuni sistemi (ad esempio, SQLite in modalità WAL), la prelazione forzata è implementata a livello di singole operazioni, riducendo il rischio di Deadlock.
Esiste una catena chiusa di thread, ciascuno in attesa di una risorsa trattenuta dal successivo nella catena. Ad esempio, il thread A trattiene la risorsa 1 e attende la risorsa 2, il thread B trattiene la risorsa 2 e attende la risorsa 1. Questa è l'unica condizione che uno sviluppatore può eliminare architetturalmente — attraverso una gerarchia di blocchi. Se tutti i thread acquisiscono risorse in un ordine globale strettamente definito, un ciclo è fisicamente impossibile.
In pratica, nelle applicazioni Android, Deadlock si verifica più spesso a causa dell'intersezione implicita di blocchi di diversi livelli: blocco del database (Room), blocco SharedPreferences e blocco della collezione in memoria. Ciascuno di questi blocchi è gestito da componenti diversi e, senza un protocollo centralizzato dell'ordine di acquisizione, gli sviluppatori creano involontariamente cicli.
Consideriamo un esempio classico di blocco reciproco — due thread acquisiscono blocchi in ordine diverso. Se il primo thread blocca la risorsa A e tenta di acquisire B, e il secondo blocca B e tenta di acquisire A, si verifica Deadlock.
class DeadlockExample {
private val lockA = Any()
private val lockB = Any()
fun operationA() {
synchronized(lockA) {
Thread.sleep(50) // simulazione del lavoro
synchronized(lockB) {
println("operationA completata")
}
}
}
fun operationB() {
synchronized(lockB) { // ordine inverso
Thread.sleep(50)
synchronized(lockA) {
println("operationB completata")
}
}
}
}
fun main() {
val ex = DeadlockExample()
Thread { ex.operationA() }.start()
Thread { ex.operationB() }.start()
// L'applicazione si bloccherà per sempre — Deadlock!
}
In questo esempio, operationA acquisisce lockA, e operationB acquisisce lockB. Quindi ciascuno tenta di acquisire il secondo blocco — ed entrambi attendono indefinitamente. Il programma si blocca senza un messaggio di errore. L'unico modo per risolvere è garantire lo stesso ordine di acquisizione dei blocchi in tutti i metodi.
Questi tre problemi di concorrenza sono spesso confusi, ma i loro meccanismi e conseguenze sono fondamentalmente diversi. Deadlock — arresto completo, Starvation — attesa infinita di una risorsa, Livelock — inattività attiva. Comprendere le differenze è criticamente importante per scegliere la giusta strategia di risoluzione.
| Caratteristica | Deadlock | Starvation | Livelock |
|---|---|---|---|
| Stato dei thread | Bloccati (BLOCKED) | Pronti (RUNNABLE) | Attivi (RUNNABLE) |
| Esecuzione del lavoro | No | No | Sì, ma inutile |
| Causa | Attesa circolare | Pianificazione ingiusta | Gestione errata dei conflitti |
| Rilevamento | Thread Dump, timeout | Monitoraggio progressi | Contatore tentativi |
Starvation (inedia) si verifica quando lo scheduler posticipa costantemente l'esecuzione di un thread a bassa priorità a favore di altri. A differenza di Deadlock, il thread non è bloccato — è pronto per l'esecuzione ma non ottiene tempo CPU. In Android, uno scenario tipico è un thread in background a bassa priorità che non viene mai eseguito se il thread UI e i thread Service sono costantemente attivi.
Livelock è una situazione in cui i thread non sono bloccati ma reagiscono infinitamente alle azioni reciproche senza svolgere lavoro utile. L'analogia classica — due persone si incontrano in un corridoio ed entrambe cercano di farsi da parte, muovendosi nella stessa direzione. A differenza di Deadlock, i thread in Livelock consumano CPU, scaricando la batteria del dispositivo.
Thread Dump è lo strumento principale per rilevare blocchi reciproci in JVM e Android Runtime. Durante un dump, la JVM analizza automaticamente il grafo delle dipendenze tra i monitor e segna i cicli di Deadlock. In Android Studio, i dump dei thread possono essere ottenuti tramite Android Profiler o il comando kill -3 PID da ADB Shell.
Il rilevamento automatico di Deadlock in fase di esecuzione è implementato tramite timer Watchdog. Se un thread non completa un'operazione entro un timeout specificato, il watchdog avvia un dump e invia un rapporto al sistema di Crash Reporting (Firebase Crashlytics, Sentry). Secondo Sentry (Issue Resolution Report, 2024), configurare un watchdog riduce il tempo di diagnosi di Deadlock da settimane a poche ore.
Durante lo sviluppo, sono efficaci l'analizzatore statico ThreadSafe di JetBrains e Checker Framework con il modulo Lock Checker. Questi strumenti analizzano l'ordine di acquisizione dei blocchi a livello di codice sorgente e avvisano di potenziali cicli. Inoltre, si raccomanda Test-Driven Deadlock Detection — test di stress che eseguono operazioni con diversi ordini di blocco in centinaia di thread.
Attenzione speciale merita la Cooperative Deadlock Detection — un metodo in cui i thread scambiano informazioni sui blocchi acquisiti tramite un registro globale. Se un thread rileva un potenziale ciclo, rilascia tutte le risorse e riprova l'operazione. Questo approccio è utilizzato nei sistemi distribuiti (Apache ZooKeeper, Google Chubby) e sta gradualmente venendo adottato nello sviluppo mobile tramite librerie come Jetpack Sync.
Il modo più affidabile è stabilire un ordine globale di acquisizione dei blocchi in tutta l'applicazione. Se tutti i thread acquisiscono sempre prima il blocco con il numero più piccolo e poi quello con il numero più grande, l'attesa circolare (condizione Circular Wait) è impossibile. Nei grandi progetti, l'ordine è documentato e verificato tramite code review.
TryLock è un metodo di blocco che non blocca un thread indefinitamente ma restituisce false se il blocco non viene acquisito entro un tempo specificato. In Java, ciò è implementato tramite ReentrantLock.tryLock(timeout, TimeUnit), in Kotlin Coroutines — tramite Mutex.withLock con timeout. In caso di fallimento, il thread rilascia tutte le risorse acquisite e riprova più tardi.
Algoritmo del banchiere è un metodo teorico di prevenzione di Deadlock proposto da Edsger Dijkstra. Modella l'allocazione delle risorse come transazioni bancarie: il sistema non alloca una risorsa se ciò potrebbe portare a uno stato non sicuro (deadlock). In pratica, l'algoritmo è raramente utilizzato nello sviluppo mobile a causa della difficoltà di conoscere in anticipo le necessità massime dei thread, ma i suoi principi sono utilizzati nei database SQLite e nei file system.
Domande frequenti
No, il blocco reciproco richiede almeno due thread. Nel codice a thread singolo, tutte le operazioni vengono eseguite sequenzialmente, quindi l'attesa circolare è impossibile. Tuttavia, Deadlock può verificarsi tra processi quando si utilizzano blocchi di file o semafori interprocesso.
Nelle coroutine, Deadlock si verifica a livello di funzioni suspend e non blocca il thread del SO, rendendolo meno evidente. Mutex di kotlinx.coroutines è un blocco sospendente (suspending) — non blocca il thread, ma la coroutine non viene eseguita. Per il rilevamento, utilizzare DebugProbes del modulo kotlinx-coroutines-debug.
Deadlock in SQLite si verifica quando due connessioni al database tentano di eseguire transazioni in ordini diversi. SQLite rileva tali situazioni e restituisce il codice di errore SQLITE_BUSY o SQLITE_LOCKED. Su Android, si raccomanda di utilizzare Room con una singola istanza del database e transazioni tramite @Transaction, eliminando Deadlock tra connessioni.
Android Runtime ha un rilevatore di Deadlock integrato che viene eseguito quando viene generato un ANR (Application Not Responding). Il sistema analizza il Thread Dump di tutti i thread dell'applicazione e segna i blocchi reciproci. Il risultato è disponibile in /data/anr/traces.txt e in Google Play Console nella sezione ANR Reports.
Prima di tutto, ottenere un Thread Dump di tutti i thread dell'applicazione. Analizzare quali blocchi ogni thread trattiene e quali tenta di acquisire. Implementare un timer Watchdog con dump automatico al superamento del limite di tempo. Dopo la correzione, aggiungere la regola lint ThreadSafety alla pipeline CI per prevenire recidive.
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