Deadlock in Java
- Deadlock è una situazione in cui due o più thread si bloccano a vicenda, impedendo il progresso del programma.
- Si verifica quando due thread hanno una dipendenza circolare su risorse sincronizzate.
- Il deadlock è difficile da rilevare e risolvere, poiché può verificarsi raramente e in condizioni specifiche.
- Non esiste un meccanismo automatico in Java per rilevare il deadlock; è necessario gestirlo manualmente.
- È possibile utilizzare il dump dello stack per identificare i thread coinvolti in un deadlock e le risorse che stanno bloccando.
Deadlock
Un tipo speciale di errore che è necessario evitare e che si riferisce specificamente al multitasking è il deadlock, che si verifica quando due thread hanno una dipendenza circolare su una coppia di oggetti sincronizzati.
Ad esempio, supponiamo che un thread entri nel monitor sull'oggetto X
e un altro thread entri nel monitor sull'oggetto Y
. Se il thread in X
cerca di chiamare qualsiasi metodo sincronizzato su Y
, si bloccherà come previsto.
Tuttavia, se il thread in Y
, a sua volta, cerca di chiamare qualsiasi metodo sincronizzato su X
, il thread aspetta per sempre, perché per accedere a X
, dovrebbe rilasciare il proprio lock su Y
in modo che il primo thread possa completare.
Il deadlock è un errore difficile da scoprire e da risolvere principalmente per i seguenti motivi:
- In generale, si verifica solo raramente, quando i due thread vengono temporizzati proprio nel modo giusto. In altre parole, è difficile riprodurre le condizioni che causano il deadlock.
- Può coinvolgere più di due thread e due oggetti sincronizzati. Cioè, il deadlock può verificarsi attraverso una sequenza di eventi più contorta di quella appena descritta.
Inoltre, il sistema di runtime di Java non fornisce alcun meccanismo per rilevare il deadlock. Quindi non bisogna aspettarsi che venga lanciata un'eccezione o che il programma venga terminato in modo anomalo quando si verifica un deadlock. Invece, il programma rimarrà bloccato fino a quando non verrà terminato manualmente.
Per comprendere completamente il deadlock, è utile vederlo in azione.
L'esempio successivo crea due classi, A
e B
, con metodi foo()
e bar()
, rispettivamente, che si fermano brevemente prima di cercare di chiamare un metodo nell'altra classe. La classe principale, chiamata Deadlock
, crea un'istanza A
e una B
, e poi chiama deadlockStart()
per avviare un secondo thread che imposta la condizione di deadlock. I metodi foo()
e bar()
usano sleep()
come modo per forzare il verificarsi della condizione di deadlock.
// Un esempio di deadlock.
// Primo oggetto sincronizzato.
class A {
// Il metodo foo() è sincronizzato e accetta un oggetto B come parametro.
synchronized void foo(B b) {
String nome = Thread.currentThread().getName();
System.out.println(nome + " è entrato in A.foo");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("A Interrotto");
}
// Dopo una pausa, il thread corrente cerca di
// chiamare il metodo ultimo() su un oggetto B passato come parametro.
// Questo può causare un deadlock se il thread B sta cercando di chiamare
// il metodo ultimo() su un oggetto A.
System.out.println(nome + " sta cercando di chiamare B.ultimo()");
b.ultimo();
}
// Il metodo ultimo() è sincronizzato e stampa un messaggio.
synchronized void ultimo() {
System.out.println("Dentro A.ultimo");
}
}
// Secondo oggetto sincronizzato.
class B {
// Il metodo bar() è sincronizzato e accetta un oggetto A come parametro.
synchronized void bar(A a) {
String nome = Thread.currentThread().getName();
System.out.println(nome + " è entrato in B.bar");
try {
Thread.sleep(1000);
} catch(Exception e) {
System.out.println("B Interrotto");
}
// Dopo una pausa, il thread corrente cerca di
// chiamare il metodo ultimo() su un oggetto A passato come parametro.
// Questo può causare un deadlock se il thread A sta cercando di chiamare
// il metodo ultimo() su un oggetto B.
System.out.println(nome + " sta cercando di chiamare A.ultimo()");
a.ultimo();
}
// Il metodo ultimo() è sincronizzato e stampa un messaggio.
synchronized void ultimo() {
System.out.println("Dentro B.ultimo");
}
}
// Classe principale che crea un deadlock tra A e B.
// Crea due thread: uno principale e uno secondario.
// Il thread principale chiama il metodo foo() su A,
// mentre il thread secondario chiama il metodo bar() su B.
// Entrambi i metodi cercano di chiamare il metodo ultimo()
// sull'oggetto dell'altro thread,
// creando una situazione di deadlock.
class Deadlock implements Runnable {
A a = new A();
B b = new B();
Thread t;
Deadlock() {
Thread.currentThread().setName("ThreadPrincipale");
t = new Thread(this, "ThreadInCorsa");
}
void deadlockStart() {
t.start();
// Il thread principale chiama il metodo foo() su A,
// passando l'oggetto B come parametro.
a.foo(b);
// Dopo che il thread principale ha chiamato foo(),
// il thread secondario (ThreadInCorsa) entrerà in B.bar(),
// creando una situazione di deadlock.
System.out.println("Di nuovo nel thread principale");
}
public void run() {
// Il thread secondario chiama il metodo bar() su B,
// passando l'oggetto A come parametro.
b.bar(a);
System.out.println("Di nuovo nell'altro thread");
}
public static void main(String[] args) {
Deadlock dl = new Deadlock();
dl.deadlockStart();
}
}
Quando eseguiamo questo programma, creeremo un deadlock tra i due thread. Il thread principale chiama il metodo foo()
sull'oggetto A
, passando l'oggetto B
come parametro. Allo stesso tempo, il thread secondario chiama il metodo bar()
sull'oggetto B
, passando l'oggetto A
come parametro.
Questo crea una situazione in cui il thread principale possiede il monitor su a
e sta cercando di chiamare il metodo ultimo()
su b
, mentre il thread secondario possiede il monitor su b
e sta cercando di chiamare il metodo ultimo()
su a
. Entrambi i thread sono bloccati in attesa l'uno dell'altro, creando un deadlock.
Quando eseguiamo questo programma, vedremo l'output mostrato qui, anche se il fatto che A.foo()
o B.bar()
si esegua per primo varierà in base all'ambiente di esecuzione specifico.
ThreadPrincipale è entrato in A.foo
ThreadInCorsa è entrato in B.bar
ThreadPrincipale sta cercando di chiamare B.ultimo()
ThreadInCorsa sta cercando di chiamare A.ultimo()
Poiché il programma è in deadlock, è necessario premere Ctrl+C per terminare il programma.
Sia sotto Windows che sotto Linux è possibile, tuttavia, ottenere un dump dello stack dei thread in esecuzione per vedere cosa sta succedendo:
- Sotto Windows, è possibile utilizzare la combinazione di tasti Ctrl+Break.
-
Sotto Linux, invece, da un'altra shell è necessario inviare il segnale
SIGQUIT
al processo Java in esecuzione. Per fare ciò, è possibile utilizzare il comandokill
seguito dall'ID del processo Java. Ad esempio, se l'ID del processo è1234
, il comando sarà:kill -SIGQUIT 1234
Il dump dello stack mostrerà i thread in esecuzione e i monitor che stanno possedendo, evidenziando il deadlock. Ecco un esempio di output del dump dello stack:
Found one Java-level deadlock:
=============================
"ThreadPrincipale":
waiting to lock monitor 0x00007ff2f4182220 (object 0x000000071b2157e0, a B),
which is held by "ThreadInCorsa"
"ThreadInCorsa":
waiting to lock monitor 0x00007ff24c007030 (object 0x000000071b214818, a A),
which is held by "ThreadPrincipale"
Java stack information for the threads listed above:
===================================================
"ThreadPrincipale":
at B.ultimo(Deadlock.java:54)
- waiting to lock <0x000000071b2157e0> (a B)
at A.foo(Deadlock.java:21)
- locked <0x000000071b214818> (a A)
at Deadlock.deadlockStart(Deadlock.java:81)
at Deadlock.main(Deadlock.java:98)
"ThreadInCorsa":
at A.ultimo(Deadlock.java:26)
- waiting to lock <0x000000071b214818> (a A)
at B.bar(Deadlock.java:49)
- locked <0x000000071b2157e0> (a B)
at Deadlock.run(Deadlock.java:92)
at java.lang.Thread.runWith(java.base@21.0.7/Thread.java:1596)
at java.lang.Thread.run(java.base@21.0.7/Thread.java:1583)
Found 1 deadlock.
Come si può osservare, ThreadInCorsa
possiede il monitor su b
, mentre sta aspettando il monitor su a
. Allo stesso tempo, ThreadPrincipale
possiede a
e sta aspettando di ottenere b
. Questo programma non si completerà mai.
In generale, se un programma Java multithreaded si blocca occasionalmente, la prima cosa da controllare è se si sta verificando un deadlock. Se si sospetta che un deadlock sia la causa, è possibile utilizzare il dump dello stack per confermare la presenza del deadlock e identificare i thread coinvolti.