synchronized
. On peut poser le mot-clé
synchronized
de trois façons. Pour chacune de ces façons, le bloc synchronisé fonctionne avec un paramètre de synchronisation.
synchronized
en tant que modificateur d'une méthode statique. Dans ce cas le paramètre de synchronisation est la classe qui possède ce bloc.
this
.
synchronized
. Dans ce cas, le paramètre de synchronisation doit être passé explicitement à l'ouverture de ce bloc. Ce peut être tout objet Java.
Exemple 64. Déclaration d'un bloc synchronisé
public class Thermostat { private Object key = new Object() ; // méthode statique synchronisée, le paramètre de synchronisation est // l'objet Thermostat.class public static synchronized boolean getNombreThermostats() { // corps de la méthode } // méthode non statique synchronisée, le paramètre de synchronisation est // l'objet this public synchronized boolean plusChaud() { // corps de la méthode } public boolean plusFroid() { // synchronization sur l'objet key // on peut aussi synchroniser sur l'objet this synchronized(key) { // bloc synchronisé } } }
Object
. Lorsque le langage Java a été conçu, cette idée était considérée comme excellente, aujourd'hui elle est plutôt battue en brêche... Il n'empêche, cela explique que tout objet Java peut être utilisé pour garder un bloc d'instructions.
Ce mécanisme de synchronization permet de résoudre le problème de concurrence d'accès que nous avons exposé. Cela dit, il pose un problème de performance. Effectivement, si un grand nombre de
threads
sont en concurrence d'exécution, tout bloc synchronisé devient un goulet d'étranglement dans lequel les
thread
ne peuvent passer qu'un par un.
L'accès à l'état d'un objet, et la modification de cet état, est un des quelques problèmes cruciaux de la programmation concurrente. Jusqu'à la version 4, Java n'exposait pas beaucoup d'éléments pour traiter ces questions. Depuis la version 5, l'API s'est considérablement enrichie, comme nous allons le voir.
T1
se trouve dans un bloc synchronisé
B1
, et est en attente à l'entrée d'un autre bloc synchronisé
B2
. Malheureusement, dans ce bloc
B2
, se trouve déjà un
thread
T2
, en attente de pouvoir entrer dans
B1
.
Ces deux
threads
s'attendent mutuellement, et ne peuvent exécuter le moindre code. La situation est bloquée, la seule façon de la débloquer est d'interrompre un des deux
threads
.
Construisons un exemple de
deadlock
, afin d'illustrer la situation. Cet exemple fonctionnera avec deux classes : une classe
Verre
et une classe
Carafe
. Lorsque le verre se remplit, la carafe se vide, et réciproquement.
Exemple 65. Deadlock : classe
Verre
public class Verre { // permet de savoir si le verre est vide ou pas private boolean plein = false ; // un verre a besoin d'une carafe pour se remplir private Carafe carafe ; // on ne peut se remplir que si la carafe est pleine public synchronized void remplis() { if (carafe.isPlein()) { plein = true ; carafe.setPlein(false) ; } } // et l'on ne peut se vider que si la carafe est vide public synchronized void vide() { if (!carafe.isPlein()) { plein = false ; carafe.setPlein(true) ; } } // permet au monde extérieur de sa voir // si nous sommes plein ou vide public synchronized boolean isPlein() { return this.plein ; } // reste des getters non synchronisé public void setPlein(boolean plein) { this.plein = plein ; } public Carafe getCarafe() { return carafe ; } }
Carafe
ressemble beaucoup à la classe
Verre
.
Exemple 66. Deadlock : classe
Carafe
public class Carafe { // permet de savoir si la carafe est vide ou pas private boolean plein = false ; // un verre a besoin d'un verre pour se vider private Verre verre ; // on ne peut se remplir que si le verre est plein public synchronized void remplis() { if (verre.isPlein()) { plein = true ; verre.setPlein(false) ; } } // et l'on ne peut se vider que si le verre est vide public synchronized void vide() { if (!verre.isPlein()) { plein = false ; verre.setPlein(true) ; } } // permet au monde extérieur de sa voir // si nous sommes plein ou vide public synchronized boolean isPlein() { return this.plein ; } // reste des getters non synchronisé public void setPlein(boolean plein) { this.plein = plein ; } public Verre getVerre() { return verre ; } }
Exemple 67. Deadlock : application
// dans une méthode main // initialisation d'une carafe et d'un verre, associés l'un à l'autre final Carafe carafe = new Carafe() ; final Verre verre = new Verre() ; carafe.setVerre(verre) ; verre.setCarafe(carafe) ; // création d'une instance de Runnable qui remplit et vide la carafe // dans le verre // on peut augmenter le nombre de cycles dans la boucle for Runnable carafeApp = new Runnable() { public void run() { for (int i = 0 ; i < 20 ; i++) { carafe.remplis() ; carafe.vide() ; } } } ; // création d'une instance de Runnable qui remplit et vide le verre // dans la carafe // on peut augmenter le nombre de cycles dans la boucle for Runnable verreApp = new Runnable() { public void run() { for (int i = 0 ; i < 20 ; i++) { verre.remplis() ; verre.vide() ; } } } ; // création des threads Thread [] carafeApps = new Thread [5] ; Thread [] verreApps = new Thread [5] ; // creation des threads pour le vidage de la carafe // et du verre for (int i = 0 ; i < carafeApps.length ; i++) { carafeApps[i] = new Thread(carafeApp) ; verreApps[i] = new Thread(verreApp) ; } // lancement des threads for (int i = 0 ; i < carafeApps.length ; i++) { carafeApps[i].start() ; verreApps[i].start() ; } // attente de la fin de l'exécution des threads for (int i = 0 ; i < carafeApps.length ; i++) { carafeApps[i].join() ; verreApps[i].join() ; }
Thread-4
et
Thread-7
.
Le
Thread-4
est en train d'exécuter la méthode
Carafe.vide()
, qui appelle
Verre.isPlein()
. Ce
thread
possède le verrou de l'objet
Carafe
dont l'ID est 19, et il attend celui de l'objet
Verre
dont l'ID est 28. Il ne peut pas entrer dans le corps de la méthode
Verre.isPlein()
tant qu'il n'a pas ce verrou.
Le
Thread-7
est en train d'exécuter la méthode
Verre.remplis()
qui appelle
Carafe.isPlein()
. Il possède le verrou de l'objet
Verre
dont l'ID est 28. C'est ce verrou que le
Thread-4
attend. Et il attend le verrou de l'objet
Carafe
dont l'ID est 29. Ce verrou est possédé par le
Thread-4
.
On remarquera qu'Eclipse a mis ces deux
threads
en rouge, car il a détecté la situation de
deadlock
.
Dans ce contexte, la seule façon de progresser est d'interrompre l'un des deux
threads
.
synchronized
d'une application.
synchronized
ne devrait jamais être exposé. En particulier, synchroniser un bloc sur
this
ou sur la classe englobante est une très mauvaise idée. Le plus souvent, n'importe quel autre objet de l'application pourra obtenir une référence sur cet objet de synchronisation, et s'il lui vient la mauvaise idée de l'utiliser pour synchroniser d'autres blocs, des situations de
deadlock
pourront arriver.
Il faut également se rappeler que les chaînes de caractères sont traitées de façon particulière par le compilateur. Ne jamais tenter de synchroniser un bloc en utilisant une chaîne de caractères est un principe absolu.