1. Approfondiamo il deadlock
Deadlock («blocco reciproco») — è una situazione in cui due o più thread si aspettano a vicenda all'infinito: ciascuno detiene una risorsa e tenta di ottenerne un'altra, già occupata dall'altro thread. Di conseguenza nessuno può proseguire e il programma «si blocca». È come due auto su un ponte stretto, muso contro muso: finché una non fa retromarcia — nessuno passa.
In Java il deadlock non è un'eccezione (Exception), ma un «blocco» del programma. I thread non terminano e non falliscono con un errore, ma semplicemente si aspettano a vicenda all'infinito. Per questo tali problemi sono insidiosi: si manifestano solo «in determinate circostanze».
Quattro condizioni perché si verifichi un deadlock
- 1. Mutua esclusione (Mutual Exclusion)
Una risorsa può essere acquisita da un solo thread alla volta (per esempio, un oggetto-monitor, un file). - 2. Possesso e attesa (Hold and Wait)
Il thread detiene una risorsa e tenta di ottenerne una seconda senza rilasciare la prima. - 3. Nessuna prelazione (No Preemption)
La risorsa non può essere «sottratta» al thread: solo il thread stesso può rilasciarla. - 4. Attesa circolare (Circular Wait)
Esiste una catena chiusa in cui ogni thread attende una risorsa detenuta dal successivo nella catena.
Un deadlock è possibile solo se TUTTE queste condizioni sono soddisfatte contemporaneamente. È sufficiente violarne almeno una — e il blocco reciproco non può verificarsi.
2. Esempio di codice con deadlock
Schema
- Ci sono due risorse: lock1 e lock2 (oggetti normali).
- Il thread A acquisisce prima lock1, poi tenta di ottenere lock2.
- Il thread B acquisisce prima lock2, poi tenta di ottenere lock1.
Esempio (non fatelo a casa!)
public class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// Thread 1
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1: ha acquisito lock1");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 1: tenta di acquisire lock2");
synchronized (lock2) {
System.out.println("Thread 1: ha acquisito lock2");
}
}
});
// Thread 2
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2: ha acquisito lock2");
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
System.out.println("Thread 2: tenta di acquisire lock1");
synchronized (lock1) {
System.out.println("Thread 2: ha acquisito lock1");
}
}
});
thread1.start();
thread2.start();
}
}
Cosa accadrà all'avvio?
- Il thread 1 acquisisce lock1, il thread 2 — lock2.
- Entrambi «si addormentano» per 100 millisecondi, facendo in tempo ad acquisire il primo lock.
- Poi ciascuno tenta di acquisire il secondo lock, già detenuto dall'altro thread.
- Entrambi i thread si aspettano a vicenda all'infinito — si è creato un deadlock.
In console vedrete:
Thread 1: ha acquisito lock1
Thread 2: ha acquisito lock2
Thread 1: tenta di acquisire lock2
Thread 2: tenta di acquisire lock1
Perché si verifica un deadlock? Analisi dettagliata
- Mutua esclusione: ogni lock può essere detenuto da un solo thread.
- Possesso e attesa: il thread 1 tiene lock1 e attende lock2; il thread 2 tiene lock2 e attende lock1.
- Assenza di prelazione: nessuno può «strappare» il lock dall'esterno — si libera solo uscendo dal blocco synchronized.
- Attesa circolare: l'attesa si è chiusa in un ciclo tra i due thread.
3. Come evitare e prevenire i deadlock
Acquisire le risorse sempre nello stesso ordine
Regola principale: se più thread devono acquisire più risorse — fatelo sempre nello stesso ordine. Per esempio: prima lock1, poi lock2 — in tutti i punti del codice.
public void doSomething() {
Object firstLock = lock1;
Object secondLock = lock2; // ordine unico "lock1 -> lock2"
synchronized (firstLock) {
synchronized (secondLock) {
// Lavoro con entrambe le risorse
}
}
}
Con un ordine unico l'attesa circolare è impossibile — la condizione del deadlock viene infranta.
Uso di tryLock con timeout (ReentrantLock)
Quando non è noto in anticipo quali risorse serviranno, usate ReentrantLock e il metodo tryLock per evitare attese infinite.
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
public class TryLockDemo {
private static final ReentrantLock lock1 = new ReentrantLock();
private static final ReentrantLock lock2 = new ReentrantLock();
public void doWork() {
try {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// Sezione critica
} finally {
lock2.unlock();
}
} else {
System.out.println("Impossibile acquisire lock2, effettuiamo il rollback");
}
} finally {
lock1.unlock();
}
} else {
System.out.println("Impossibile acquisire lock1, effettuiamo il rollback");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Pro: si può annullare l'operazione e riprovare più tardi. Contro: il codice è più complesso, ma il deadlock è escluso.
Minimizzate il tempo di detenzione dei lock
Tenete il lock il meno possibile. All'interno di synchronized/lock fate solo l'indispensabile; tutto il resto spostatelo fuori dalla sezione critica.
Evitate lock annidati
Meno annidamenti di synchronized/lock, minore è il rischio di blocco reciproco. Se possibile — usate un solo lock per risorse correlate.
Usate strutture pronte thread-safe
La libreria standard ha già risolto molti problemi: collezioni come ConcurrentHashMap e altre classi di java.util.concurrent riducono il rischio di deadlock e semplificano il codice.
4. Diagnostica dei deadlock
Thread Dump e jstack
Il Thread Dump è un'istantanea dello stato di tutti i thread nella JVM.
Acquisizione da console:
jstack <pid>
Dove <pid> è l'identificatore del processo Java (si può ottenere con jps).
Nell'IDE di solito c'è il pulsante «Thread Dump» nel pannello di debug.
Nel dump cercate:
- Gli stati BLOCKED o WAITING.
- Messaggi del tipo waiting to lock ... e locked ....
- La frase della JVM: "Found one Java-level deadlock:" — quando viene rilevato un blocco reciproco.
Esempio di estratto di dump:
"Thread-1":
waiting to lock monitor 0x000000001e4000, (object 0x7f8a5c00, a java.lang.Object),
which is held by "Thread-2"
"Thread-2":
waiting to lock monitor 0x000000001e3000, (object 0x7f8a5c10, a java.lang.Object),
which is held by "Thread-1"
Questo è proprio un deadlock: i thread si «tengono» a vicenda.
VisualVM e altri strumenti
- VisualVM — uno strumento gratuito per analizzare i thread e rilevare i deadlock.
- Java Mission Control / Flight Recorder — monitoraggio e profilazione avanzati.
In VisualVM è possibile vedere l'albero dei thread, i loro stati e ricevere una notifica di blocco reciproco.
Tabella: «Come evitare di finire in un deadlock»
| Causa del deadlock | Come evitarlo |
|---|---|
| Acquisizione delle risorse in ordine diverso | Attenersi sempre a un unico ordine di acquisizione delle risorse |
| synchronized annidati | Minimizzare l'annidamento, se possibile usare un solo lock |
| Detenzione del lock per troppo tempo | Ridurre il lavoro all'interno della sezione critica |
| Uso simultaneo di più lock | Usare tryLock con timeout e saper fare rollback |
| Collezioni non thread-safe | Usare collezioni concorrenti (ConcurrentHashMap ecc.) |
5. Errori tipici nella gestione dei deadlock
Errore n. 1: acquisizione delle risorse in ordine diverso. La causa più comune — thread diversi prendono i lock in un ordine diverso. Anche se «così è più veloce», rispettate un ordine unico — altrimenti l'attesa circolare è garantita.
Errore n. 2: synchronized annidati senza necessità. Un annidamento superfluo aumenta il rischio di blocco reciproco e complica il codice. Semplificate il modello di locking.
Errore n. 3: ignorare tryLock e i timeout. Il locking tramite synchronized costringerà il thread ad attendere all'infinito. Se non c'è certezza, usate tryLock con timeout e una logica di rollback.
Errore n. 4: esecuzione prolungata del codice all'interno del lock. Richieste di rete, I/O e calcoli pesanti sotto lock aumentano drasticamente la probabilità di problemi. Spostateli fuori dalla sezione critica.
Errore n. 5: non analizzare il Thread Dump. Thread Dump è il tuo miglior alleato nella ricerca di blocchi reciproci. Usa jstack e analizza gli stati BLOCKED/WAITING, le catene «holding/waiting to lock».
GO TO FULL VERSION