Le mot clé synchronized
Il peut arriver que plusieurs threads aient besoin de modifier quelque chose de commun : un fichier, une base de données, une instance d'une classe, etc. Si plusieurs threads modifient en même temps, il pourrait arriver des résultats non désirables.
Par exemple, si plusieurs threads voulaient lire et écrire dans un fichier simultanément :
graph TB
fichier[(Fichier)]
thread1
thread2
thread3
thread1 --> fichier --> thread1
thread2 --> fichier --> thread2
thread3 --> fichier --> thread3
Étant donné que les threads fonctionnent simultanément, si un thread tente de lire pendant que l'autre écrire, peut-être qu'il n'aura pas accès. Ou si un thread voulait écrire du contenu pendant qu'un autre le lise, ce dernier n'aurait pas la dernière information disponible. Il faut donc remédier au problème.
Le mot clé synchronized
L'utilisation du mot clé synchronized
permet justement d'indiquer à Java que le bout de code ne peut pas être exécuté par plusieurs threads en même temps. Il joue un peu le rôle d'une file d'attente.
flowchart TB
fichier[(Fichier)]
thread1
thread2
thread3
s{{synchronized}}
thread1 --> s
thread2 --> s
thread3 --> s
s --> fichier
Il est possible d'appliquer le mot synchronized
sur une méthode entière ou simplement une partie de code.
Méthode synchronisée
Pour synchroniser une méthode, on place le mot synchronized
immédiatement après l'accessibilité (public, private, etc.).
public synchronized void bonjour() {
// Dormir une seconde
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Afficher
System.out.println("Bonjour");
}
Maintenant, si plusieurs threads voulaient exécuter la méthode bonjour()
, ils ne pourraient le faire qu'un thread à la fois.
Essayez-le
Essayez le code ci-dessous avec et sans le mot clé synchronized
- Enlevez
synchronized
et comparez les résultats
Il est très important de noter que synchronized
fonctionne seulement si tous les threads utilisent la même instance de l'objet. Si la fonction main()
était changée pour le code suivant, ça ne fonctionnerait pas :
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
Thread monThread = new Thread(new Message());
monThread.start();
}
}
Pourtant, on pourrait croire que le résultat est le même car la méthode bonjour()
devrait être exécutée que par un seul thread à la fois. Que se passe-t-il ?
Voici une représentation graphique :
flowchart TB
subgraph Non Fonctionnel
tt1[thread1]
tt2[thread2]
tt3[thread3]
mm1{{message}} -- synchronized --> bb1(("bonjour()"))
mm2{{message}} -- synchronized --> bb2(("bonjour()"))
mm3{{message}} -- synchronized --> bb3(("bonjour()"))
tt1 --> mm1
tt2 --> mm2
tt3 --> mm3
end
subgraph Fonctionnel
m{{message}}
b(("bonjour()"))
t1[thread1]
t2[thread2]
t3[thread3]
m -- synchronized --> b
t1 --> m
t2 --> m
t3 --> m
end
En créant chaque fois des nouveaux objets dans un thread différent, chaque thread exécutera une méthode provenant d'un objet différent à chaque fois.
Bloc de code synchronisé
Il peut arriver que seulement une partie d'une méthode doive être synchronisée :
public void bonjour() {
System.out.println("Avant");
synchronized (this) {
System.out.println("Bonjour 1");
System.out.println("Bonjour 2");
System.out.println("Bonjour 3");
}
System.out.println("Après");
}
Dans cet exemple, seulement un thread aura accès à afficher les bonjours à la fois. Les autres threads devront attendre que tout le code soit terminé avant de pouvoir continuer.
À l'intérieur de synchronized
, il y a le mot this
qui représente l'instance de l'objet. On peut remplacer this
par n'importe instance de n'importe quel objet. Cette portion représente un verrou à acquérir avant de continuer. Si celui-ci est disponible, le thread le prend et exécute le code. Si un second thread veut exécuter le même code, le verrou ne sera pas disponible et il devra attendre qu'il se libère.
Verrous multiples
On peut avoir plusieurs verrous si le besoin se fait sentir :
final Object o1 = new Object();
final Object o2 = new Object();
final Object o3 = new Object();
public void bonjour() {
// Dormir une seconde
try {
synchronized (o1) {
System.out.println("O1");
Thread.sleep(1000);
}
synchronized (o2) {
System.out.println("O2");
Thread.sleep(1000);
}
synchronized (o3) {
System.out.println("O3");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Afficher
System.out.println("Bonjour");
}
Dans ce scénario, le premier thread pourrait acquérir le verrou nommé o1
. Il affichera "01" et dormira pendant une seconde. Le second thread devra alors attendre la fin du Thread.sleep(1000)
avant de pouvoir se porter acquéreur du verrou o1
et aussi afficher "01". Même principe pour les verrous suivants.
Le résultat sera alors :
Imbriquer des verrous
Finalement, on peut imbriquer les verrous l'un dans l'autre. Le même principe d'obtention des verrous s'applique. La différence principale est que pour obtenir un verrou dans un autre, le verrou principal devra être acquis :
final Object o1 = new Object();
final Object o2 = new Object();
final Object o3 = new Object();
public void bonjour() {
// Dormir une seconde
try {
synchronized (o1) {
System.out.println("O1");
Thread.sleep(1000);
synchronized (o2) {
System.out.println("O2");
Thread.sleep(1000);
synchronized (o3) {
System.out.println("O3");
Thread.sleep(1000);
}
}
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Afficher
System.out.println("Bonjour");
}
- Le premier thread obtient le verrou o1 et affiche "O1".
- Le second thread attend.
- Le premier thread obtient le verrou o2 et affiche "O2".
- Le second thread attend toujours.
- Le premier thread obtient le verrou o3 et affiche "O3".
- Le second thread attend toujours.
- Le premier thread rend le verrou o3, o2 et o1 et affiche "Bonjour".
- Le second thread obtient le verrou o1 et affiche "O1".
- Ainsi de suite.
Le résultat sera alors un peu différent :
01
(pause de 1 seconde)
02
(pause de 1 seconde)
03
(pause de 1 seconde)
Bonjour
01
(pause de 1 seconde)
02
(pause de 1 seconde)
...
Note
Essayez le code ci-dessus et assuez-vous de bien comprendre non seulement la syntaxe mais également le fonctionnement de la synchronisation des threads.