Java Thread Deadlock e Livelock

1. Panoramica

Sebbene il multi-threading aiuti a migliorare le prestazioni di un'applicazione, presenta anche alcuni problemi. In questo tutorial, esamineremo due di questi problemi, deadlock e livelock, con l'aiuto di esempi Java.

2. Deadlock

2.1. Cos'è il deadlock?

Un deadlock si verifica quando due o più thread attendono per sempre un blocco o una risorsa mantenuta da un altro thread . Di conseguenza, un'applicazione potrebbe bloccarsi o non riuscire poiché i thread con deadlock non possono avanzare.

Il classico problema dei filosofi del pranzo mostra bene i problemi di sincronizzazione in un ambiente multi-thread ed è spesso usato come esempio di deadlock.

2.2. Esempio di deadlock

Innanzitutto, diamo un'occhiata a un semplice esempio Java per comprendere il deadlock.

In questo esempio, creeremo due thread, T1 e T2 . Il thread T1 chiama operazione1 e il thread T2 chiama operazioni .

Per completare le operazioni, il thread T1 deve acquisire prima lock1 e quindi lock2 , mentre il thread T2 deve acquisire prima lock2 e poi lock1 . Quindi, fondamentalmente, entrambi i thread stanno cercando di acquisire i blocchi nell'ordine opposto.

Ora scriviamo la classe DeadlockExample :

public class DeadlockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { DeadlockExample deadlock = new DeadlockExample(); new Thread(deadlock::operation1, "T1").start(); new Thread(deadlock::operation2, "T2").start(); } public void operation1() { lock1.lock(); print("lock1 acquired, waiting to acquire lock2."); sleep(50); lock2.lock(); print("lock2 acquired"); print("executing first operation."); lock2.unlock(); lock1.unlock(); } public void operation2() { lock2.lock(); print("lock2 acquired, waiting to acquire lock1."); sleep(50); lock1.lock(); print("lock1 acquired"); print("executing second operation."); lock1.unlock(); lock2.unlock(); } // helper methods }

Eseguiamo ora questo esempio di deadlock e notiamo l'output:

Thread T1: lock1 acquired, waiting to acquire lock2. Thread T2: lock2 acquired, waiting to acquire lock1.

Una volta eseguito il programma, possiamo vedere che il programma risulta in un deadlock e non esce mai. Il log mostra che il thread T1 è in attesa di lock2 , che è mantenuto dal thread T2 . Allo stesso modo, il thread T2 è in attesa di lock1 , che viene mantenuto dal thread T1 .

2.3. Evitare il deadlock

Il deadlock è un problema comune di concorrenza in Java. Pertanto, dovremmo progettare un'applicazione Java per evitare potenziali condizioni di deadlock.

Per cominciare, dovremmo evitare la necessità di acquisire più blocchi per un thread. Tuttavia, se un thread necessita di più blocchi, dovremmo assicurarci che ogni thread acquisisca i blocchi nello stesso ordine, per evitare qualsiasi dipendenza ciclica nell'acquisizione dei blocchi .

Possiamo anche utilizzare tentativi di blocco a tempo , come il metodo tryLock nell'interfaccia Lock , per assicurarci che un thread non si blocchi all'infinito se non è in grado di acquisire un blocco.

3. Livelock

3.1. Cos'è Livelock

Livelock è un altro problema di concorrenza ed è simile al deadlock. In livelock, due o più thread continuano a trasferire stati tra loro invece di aspettare all'infinito come abbiamo visto nell'esempio di deadlock. Di conseguenza, i thread non sono in grado di eseguire le rispettive attività.

Un ottimo esempio di livelock è un sistema di messaggistica in cui, quando si verifica un'eccezione, il consumatore del messaggio esegue il rollback della transazione e rimette il messaggio all'inizio della coda. Quindi lo stesso messaggio viene letto ripetutamente dalla coda, solo per causare un'altra eccezione ed essere reinserito nella coda. Il consumatore non preleverà mai nessun altro messaggio dalla coda.

3.2. Esempio di Livelock

Ora, per dimostrare la condizione di livelock, prenderemo lo stesso esempio di deadlock discusso in precedenza. Anche in questo esempio, il thread T1 chiama operazione1 e il thread T2 chiama operazione2 . Tuttavia, cambieremo leggermente la logica di queste operazioni.

Entrambi i thread necessitano di due blocchi per completare il loro lavoro. Ogni thread acquisisce il primo blocco ma rileva che il secondo blocco non è disponibile. Quindi, per lasciare che l'altro thread si completi per primo, ogni thread rilascia il suo primo blocco e tenta di acquisire nuovamente entrambi i blocchi.

Dimostriamo livelock con una classe LivelockExample :

public class LivelockExample { private Lock lock1 = new ReentrantLock(true); private Lock lock2 = new ReentrantLock(true); public static void main(String[] args) { LivelockExample livelock = new LivelockExample(); new Thread(livelock::operation1, "T1").start(); new Thread(livelock::operation2, "T2").start(); } public void operation1() { while (true) { tryLock(lock1, 50); print("lock1 acquired, trying to acquire lock2."); sleep(50); if (tryLock(lock2)) { print("lock2 acquired."); } else { print("cannot acquire lock2, releasing lock1."); lock1.unlock(); continue; } print("executing first operation."); break; } lock2.unlock(); lock1.unlock(); } public void operation2() { while (true) { tryLock(lock2, 50); print("lock2 acquired, trying to acquire lock1."); sleep(50); if (tryLock(lock1)) { print("lock1 acquired."); } else { print("cannot acquire lock1, releasing lock2."); lock2.unlock(); continue; } print("executing second operation."); break; } lock1.unlock(); lock2.unlock(); } // helper methods }

Ora, eseguiamo questo esempio:

Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: cannot acquire lock2, releasing lock1. Thread T2: cannot acquire lock1, releasing lock2. Thread T2: lock2 acquired, trying to acquire lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T1: cannot acquire lock2, releasing lock1. Thread T1: lock1 acquired, trying to acquire lock2. Thread T2: cannot acquire lock1, releasing lock2. ..

Come possiamo vedere nei log, entrambi i thread acquisiscono e rilasciano ripetutamente blocchi. Per questo motivo, nessuno dei thread è in grado di completare l'operazione.

3.3. Evitando Livelock

Per evitare un livelock, dobbiamo esaminare la condizione che causa il livelock e quindi trovare una soluzione di conseguenza.

Ad esempio, se abbiamo due thread che stanno acquisendo e rilasciando ripetutamente blocchi, con conseguente livelock, possiamo progettare il codice in modo che i thread riprovino ad acquisire i blocchi a intervalli casuali. Ciò darà ai thread una buona possibilità di acquisire le serrature di cui hanno bisogno.

Un altro modo per risolvere il problema della vivacità nell'esempio del sistema di messaggistica di cui abbiamo discusso in precedenza è inserire i messaggi non riusciti in una coda separata per ulteriori elaborazioni invece di rimetterli nuovamente nella stessa coda.

4. Conclusione

In questo tutorial, abbiamo discusso deadlock e livelock. Inoltre, abbiamo esaminato esempi Java per dimostrare ciascuno di questi problemi e accennato brevemente a come possiamo evitarli.

Come sempre, il codice completo utilizzato in questo esempio può essere trovato su GitHub.