Un bref aperçu des détails de la façon dont les threads interagissent. Auparavant, nous avons examiné comment les threads sont synchronisés les uns avec les autres. Cette fois, nous allons plonger dans les problèmes qui peuvent survenir lorsque les threads interagissent, et nous verrons comment les éviter. Nous fournirons également quelques liens utiles pour une étude plus approfondie.
Avec un plugin JVisualVM installé (via Outils -> Plugins), nous pouvons voir où le blocage s'est produit :
Selon JVisualVM, nous voyons des périodes de veille et une période de parc (c'est-à-dire lorsqu'un thread tente d'acquérir un verrou — il entre dans l'état de parc, comme nous l'avons vu précédemment lorsque nous avons parlé de la synchronisation des threads ) . Vous pouvez voir un exemple de livelock ici : Java - Thread Livelock .
Vous pouvez voir un super exemple ici : Java - Thread Starvation and Fairness . Cet exemple montre ce qui se passe avec les threads pendant la famine et comment un petit changement de à
cette inspection a été ajoutée à IntelliJ IDEA dans le cadre du problème IDEA-61117 , qui figurait dans les notes de publication en 2010.

Introduction
Donc, nous savons que Java a des threads. Vous pouvez lire à ce sujet dans la revue intitulée Mieux ensemble : Java et la classe Thread. Partie I — Threads d'exécution . Et nous avons exploré le fait que les threads peuvent se synchroniser entre eux dans la revue intitulée Better together : Java and the Thread class. Partie II — Synchronisation . Il est temps de parler de la façon dont les threads interagissent les uns avec les autres. Comment partagent-ils les ressources partagées ? Quels problèmes pourraient survenir ici?
Impasse
Le problème le plus effrayant de tous est l'impasse. L'impasse est lorsque deux threads ou plus attendent éternellement l'autre. Nous prendrons un exemple de la page Web d'Oracle qui décrit le blocage :
public class Deadlock {
static class Friend {
private final String name;
public Friend(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public synchronized void bow(Friend bower) {
System.out.format("%s: %s bowed to me!%n",
this.name, bower.getName());
bower.bowBack(this);
}
public synchronized void bowBack(Friend bower) {
System.out.format("%s: %s bowed back to me!%n",
this.name, bower.getName());
}
}
public static void main(String[] args) {
final Friend alphonse = new Friend("Alphonse");
final Friend gaston = new Friend("Gaston");
new Thread(() -> alphonse.bow(gaston)).start();
new Thread(() -> gaston.bow(alphonse)).start();
}
}
Le blocage peut ne pas se produire ici la première fois, mais si votre programme se bloque, alors il est temps de s'exécuter jvisualvm
: 
"Thread-1" - Thread t@12
java.lang.Thread.State: BLOCKED
at Deadlock$Friend.bowBack(Deadlock.java:16)
- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Le thread 1 attend le verrou du thread 0. Pourquoi cela se produit-il ? Thread-1
démarre et exécute la Friend#bow
méthode. Il est marqué du synchronized
mot-clé, ce qui signifie que nous acquérons le moniteur pour this
(l'objet actuel). L'entrée de la méthode était une référence à l'autre Friend
objet. Maintenant, Thread-1
veut exécuter la méthode sur l'autre Friend
et doit acquérir son verrou pour ce faire. Mais si l'autre thread (dans ce cas Thread-0
) a réussi à entrer dans la bow()
méthode, alors le verrou a déjà été acquis et Thread-1
attendThread-0
, et vice versa. Cette impasse est insoluble, et nous l'appelons impasse. Comme une emprise mortelle qui ne peut être relâchée, l'impasse est un blocage mutuel qui ne peut être brisé. Pour une autre explication de l'impasse, vous pouvez regarder cette vidéo : Deadlock et Livelock expliqués .
Livelock
S'il y a impasse, y a-t-il aussi livelock ? Oui, il y a :) Livelock se produit lorsque les threads semblent être vivants, mais qu'ils sont incapables de faire quoi que ce soit, car la ou les conditions requises pour qu'ils continuent leur travail ne peuvent pas être remplies. Fondamentalement, le livelock est similaire au blocage, mais les threads ne "se bloquent" pas en attendant un moniteur. Au lieu de cela, ils font toujours quelque chose. Par exemple:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class App {
public static final String ANSI_BLUE = "\u001B[34m";
public static final String ANSI_PURPLE = "\u001B[35m";
public static void log(String text) {
String name = Thread.currentThread().getName(); // Like "Thread-1" or "Thread-0"
String color = ANSI_BLUE;
int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
if (val != 0) {
color = ANSI_PURPLE;
}
System.out.println(color + name + ": " + text + color);
try {
System.out.println(color + name + ": wait for " + val + " sec" + color);
Thread.currentThread().sleep(val * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Lock first = new ReentrantLock();
Lock second = new ReentrantLock();
Runnable locker = () -> {
boolean firstLocked = false;
boolean secondLocked = false;
try {
while (!firstLocked || !secondLocked) {
firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
log("First Locked: " + firstLocked);
secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
log("Second Locked: " + secondLocked);
}
first.unlock();
second.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
};
new Thread(locker).start();
new Thread(locker).start();
}
}
Le succès de ce code dépend de l'ordre dans lequel le planificateur de threads Java démarre les threads. Si Thead-1
commence en premier, alors nous obtenons livelock :
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Comme vous pouvez le voir dans l'exemple, les deux threads tentent d'acquérir les deux verrous à tour de rôle, mais ils échouent. Mais, ils ne sont pas dans l'impasse. Extérieurement, tout va bien et ils font leur travail. 
famine
En plus du blocage et du livelock, il existe un autre problème qui peut survenir lors du multithreading : la famine. Ce phénomène diffère des formes de blocage précédentes en ce que les threads ne sont pas bloqués - ils n'ont tout simplement pas les ressources suffisantes. Par conséquent, alors que certains threads prennent tout le temps d'exécution, d'autres sont incapables de s'exécuter :
https://www.logicbig.com/
Thread.sleep()
vous Thread.wait()
permet de répartir la charge uniformément. 
Conditions de course
Dans le multithreading, il existe une "condition de concurrence". Ce phénomène se produit lorsque des threads partagent une ressource, mais que le code est écrit de manière à ne pas garantir un partage correct. Jetez un oeil à un exemple:
public class App {
public static int value = 0;
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
int oldValue = value;
int newValue = ++value;
if (oldValue + 1 != newValue) {
throw new IllegalStateException(oldValue + " + 1 = " + newValue);
}
}
};
new Thread(task).start();
new Thread(task).start();
new Thread(task).start();
}
}
Ce code peut ne pas générer d'erreur la première fois. Quand c'est le cas, cela peut ressembler à ceci:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
at App.lambda$main$0(App.java:13)
at java.lang.Thread.run(Thread.java:745)
Comme vous pouvez le voir, quelque chose s'est mal passé lors newValue
de l'attribution d'une valeur. newValue
C est trop gros. En raison de la condition de concurrence, l'un des threads a réussi à modifier la variable value
entre les deux instructions. Il s'avère qu'il y a une course entre les fils. Pensez maintenant à quel point il est important de ne pas commettre d'erreurs similaires avec des transactions monétaires... Des exemples et des diagrammes peuvent également être vus ici : Code pour simuler la condition de concurrence dans le thread Java .
Volatil
Parlant de l'interaction des threads, levolatile
mot-clé mérite d'être mentionné. Prenons un exemple simple :
public class App {
public static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
Runnable whileFlagFalse = () -> {
while(!flag) {
}
System.out.println("Flag is now TRUE");
};
new Thread(whileFlagFalse).start();
Thread.sleep(1000);
flag = true;
}
}
Plus intéressant encore, il est fort probable que cela ne fonctionne pas. Le nouveau fil ne verra pas le changement dans le flag
champ. Pour résoudre ce problème pour le flag
champ, nous devons utiliser le volatile
mot-clé. Comment et pourquoi? Le processeur effectue toutes les actions. Mais les résultats des calculs doivent être stockés quelque part. Pour cela, il y a la mémoire principale et il y a le cache du processeur. Les caches d'un processeur sont comme un petit morceau de mémoire utilisé pour accéder aux données plus rapidement que lors de l'accès à la mémoire principale. Mais tout a un revers : les données du cache peuvent ne pas être à jour (comme dans l'exemple ci-dessus, lorsque la valeur du champ flag n'a pas été mise à jour). Alors levolatile
Le mot-clé indique à la JVM que nous ne voulons pas mettre en cache notre variable. Cela permet d'afficher le résultat à jour sur tous les threads. C'est une explication très simplifiée. Quant au volatile
mot-clé, je vous recommande vivement de lire cet article . Pour plus d'informations, je vous conseille également de lire Java Memory Model et Java Volatile Keyword . De plus, il est important de se rappeler qu'il volatile
s'agit de la visibilité, et non de l'atomicité des changements. En regardant le code dans la section "Conditions de course", nous verrons une info-bulle dans IntelliJ IDEA : 
Atomicité
Les opérations atomiques sont des opérations qui ne peuvent pas être divisées. Par exemple, l'opération d'assignation d'une valeur à une variable doit être atomique. Malheureusement, l'opération d'incrémentation n'est pas atomique, car l'incrémentation nécessite jusqu'à trois opérations CPU : obtenir l'ancienne valeur, en ajouter une, puis enregistrer la valeur. Pourquoi l'atomicité est-elle importante ? Avec l'opération d'incrémentation, s'il existe une condition de concurrence, la ressource partagée (c'est-à-dire la valeur partagée) peut soudainement changer à tout moment. De plus, les opérations impliquant des structures 64 bits, par exemplelong
et double
, ne sont pas atomiques. Plus de détails peuvent être lus ici : Assurer l'atomicité lors de la lecture et de l'écriture de valeurs 64 bits . Les problèmes liés à l'atomicité peuvent être vus dans cet exemple :
public class App {
public static int value = 0;
public static AtomicInteger atomic = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
value++;
atomic.incrementAndGet();
}
};
for (int i = 0; i < 3; i++) {
new Thread(task).start();
}
Thread.sleep(300);
System.out.println(value);
System.out.println(atomic.get());
}
}
La classe spéciale AtomicInteger
nous donnera toujours 30 000, mais cela value
changera de temps en temps. Il y a un bref aperçu de ce sujet : Introduction aux variables atomiques en Java . L'algorithme "comparer et échanger" est au cœur des classes atomiques. Vous pouvez en savoir plus à ce sujet ici dans Comparaison des algorithmes sans verrouillage - CAS et FAA sur l'exemple de JDK 7 et 8 ou dans l' article Compare-and-swap sur Wikipedia. 
http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
Se passe-avant
Il existe un concept intéressant et mystérieux appelé "se passe avant". Dans le cadre de votre étude des threads, vous devriez lire à ce sujet. La relation se produit avant montre l'ordre dans lequel les actions entre les threads seront vues. Il existe de nombreuses interprétations et commentaires. Voici l'une des présentations les plus récentes sur ce sujet : Java "Happens-Before" Relationships .Résumé
Dans cette revue, nous avons exploré certaines des spécificités de l'interaction des threads. Nous avons discuté des problèmes qui peuvent survenir, ainsi que des moyens de les identifier et de les éliminer. Liste de documents supplémentaires sur le sujet :- Verrouillage à double contrôle
- FAQ JSR 133 (modèle de mémoire Java)
- QI 35 : Comment éviter une impasse ?
- Concepts de concurrence en Java par Douglas Hawkins (2017)
GO TO FULL VERSION