CodeGym /Corsi /JAVA 25 SELF /Deadlock: cause, esempi, risoluzione

Deadlock: cause, esempi, risoluzione

JAVA 25 SELF
Livello 53 , Lezione 0
Disponibile

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

Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION