Blocage des threads Java et Livelock

1. Vue d'ensemble

Bien que le multi-threading aide à améliorer les performances d'une application, il présente également certains problèmes. Dans ce didacticiel, nous examinerons deux de ces problèmes, le blocage et le blocage en direct, à l'aide d'exemples Java.

2. Impasse

2.1. Qu'est-ce que l'impasse?

Un blocage se produit lorsque deux ou plusieurs threads attendent indéfiniment un verrou ou une ressource détenue par un autre des threads . Par conséquent, une application peut se bloquer ou échouer car les threads bloqués ne peuvent pas progresser.

Le problème classique des philosophes de la restauration illustre bien les problèmes de synchronisation dans un environnement multi-thread et est souvent utilisé comme exemple de blocage.

2.2. Exemple de blocage

Tout d'abord, examinons un exemple Java simple pour comprendre le blocage.

Dans cet exemple, nous allons créer deux threads, T1 et T2 . Le thread T1 appelle opération1 et le thread T2 appelle les opérations .

Pour terminer leurs opérations, le thread T1 doit d'abord acquérir lock1 , puis lock2 , tandis que le thread T2 doit d'abord acquérir lock2 , puis lock1 . Donc, fondamentalement, les deux threads essaient d'acquérir les verrous dans l'ordre opposé.

Maintenant, écrivons 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 }

Exécutons maintenant cet exemple de blocage et notons la sortie:

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

Une fois que nous exécutons le programme, nous pouvons voir que le programme entraîne un blocage et ne se ferme jamais. Le journal montre que le thread T1 attend lock2 , qui est détenu par le thread T2 . De même, le thread T2 attend lock1 , qui est détenu par le thread T1 .

2.3. Éviter les impasses

Le blocage est un problème de concurrence courante en Java. Par conséquent, nous devons concevoir une application Java pour éviter toute situation de blocage potentiel.

Pour commencer, nous devrions éviter d'avoir à acquérir plusieurs verrous pour un thread. Cependant, si un thread a besoin de plusieurs verrous, nous devons nous assurer que chaque thread acquiert les verrous dans le même ordre, pour éviter toute dépendance cyclique dans l'acquisition des verrous .

Nous pouvons également utiliser des tentatives de verrouillage chronométrées , comme la méthode tryLock dans l' interface Lock , pour nous assurer qu'un thread ne se bloque pas indéfiniment s'il est incapable d'acquérir un verrou.

3. Livelock

3.1. Qu'est-ce que Livelock

Livelock est un autre problème de concurrence et est similaire à une impasse. Dans livelock, deux threads ou plus continuent à transférer des états entre eux au lieu d'attendre indéfiniment comme nous l'avons vu dans l'exemple de blocage. Par conséquent, les threads ne sont pas en mesure d'exécuter leurs tâches respectives.

Un bon exemple de livelock est un système de messagerie où, lorsqu'une exception se produit, le consommateur de message annule la transaction et remet le message en tête de la file d'attente. Ensuite, le même message est lu à plusieurs reprises à partir de la file d'attente, uniquement pour provoquer une autre exception et être remis dans la file d'attente. Le consommateur ne prendra jamais aucun autre message de la file d'attente.

3.2. Exemple Livelock

Maintenant, pour démontrer la condition de blocage de la vie, nous allons prendre le même exemple de blocage que nous avons discuté précédemment. Dans cet exemple également, le thread T1 appelle operation1 et le thread T2 appelle operation2 . Cependant, nous modifierons légèrement la logique de ces opérations.

Les deux threads ont besoin de deux verrous pour terminer leur travail. Chaque thread acquiert son premier verrou mais constate que le second verrou n'est pas disponible. Ainsi, afin de laisser l'autre thread se terminer en premier, chaque thread libère son premier verrou et tente à nouveau d'acquérir les deux verrous.

Démontrons livelock avec une 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 }

Maintenant, exécutons cet exemple:

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

Comme nous pouvons le voir dans les journaux, les deux threads acquièrent et libèrent des verrous à plusieurs reprises. Pour cette raison, aucun des threads n'est en mesure de terminer l'opération.

3.3. Éviter Livelock

Pour éviter un blocage de la vie, nous devons examiner la condition qui cause le blocage de la vie, puis trouver une solution en conséquence.

Par exemple, si nous avons deux threads qui acquièrent et libèrent des verrous à plusieurs reprises, ce qui entraîne un blocage en direct, nous pouvons concevoir le code de sorte que les threads réessayent d'acquérir les verrous à des intervalles aléatoires. Cela donnera aux threads une chance équitable d'acquérir les verrous dont ils ont besoin.

Une autre façon de résoudre le problème de vivacité dans l'exemple de système de messagerie dont nous avons discuté précédemment est de placer les messages ayant échoué dans une file d'attente distincte pour un traitement ultérieur au lieu de les remettre dans la même file d'attente.

4. Conclusion

Dans ce didacticiel, nous avons discuté du blocage et du blocage en direct. En outre, nous avons examiné des exemples Java pour illustrer chacun de ces problèmes et avons brièvement abordé comment les éviter.

Comme toujours, le code complet utilisé dans cet exemple se trouve à l'adresse over sur GitHub.