(voir dernier exercice du TP précédent)
Supposons que deux threads veulent s'échanger des données...
public class Holder { private int value; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { holder.value = 42; }); System.out.println(holder.value == 42 ? "OK" : "KO"); } }
Que va afficher ce programme ?
Parfois OK... et parfois KO.
public class Holder { private int value; private boolean done; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { holder.value = 42; holder.done = true; }); while (!holder.done) { // ne rien faire ! } System.out.println(holder.value == 42 ? "OK" : "KO"); } }
Qu'est-ce qui pose problème ici ?
public class Holder { private int value; private boolean done; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { holder.value = 42; holder.done = true; }); // le code peut être optimisé pour utiliser une variable locale... var tmp = holder.done while (!tmp) { // ne rien faire ! } System.out.println(holder.value == 42 ? "OK" : "KO"); } }
Qu'est-ce qui pose problème ici ?
public class Holder { private int value; private boolean done; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { // les deux affectations peuvent être échangées... holder.done = true; holder.value = 42; }); while (!holder.done) { // ne rien faire ! } System.out.println(holder.value == 42 ? "OK" : "KO"); } }
Qu'est-ce qui pose problème ici ?
public class Holder { private int value; private boolean done; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { holder.value = 42; holder.done = true; }); while (!holder.done) { // là!!! // ne rien faire ! } System.out.println(holder.value == 42 ? "OK" : "KO"); } }
Important : l'attente active consiste à vérifier périodiquement l'état d'un objet jusqu'à observer le changement d'état désiré. Cela produit une boucle qui consomme énormément de ressources CPU. Il faut absolument éviter ce comportement.
public class Holder { private int value; private boolean done; private final Object lock = new Object(); public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { synchronized (holder.lock) { holder.value = 42; holder.done = true; } }); for (;;) { synchronized (holder.lock) { if (holder.done) { break; } } } synchronized (holder.lock) { System.out.println(holder.value == 42 ? "OK" : "KO"); } } }
Pourquoi est-ce que le dernier bloc synchronized
est nécessaire ?
Parce que, sans lui, on n'a aucune garantie que la variable value
est relue en mémoire. Comme c'est un autre thread qui fait l'écriture, elle pourrait très bien être simplement relue dans le cache et pas dans la RAM.
Ça ne résout pas le problème de l'attente active !
public class Holder { private int value; private boolean done; private final Object lock = new Object(); private void init() { synchronized (lock) { value = 42; done = true; } } private void display() { for (;;) { // c'est toujours de l'attente active... synchronized (lock) { if (done) { break; } } } synchronized (lock) { System.out.println(value == 42 ? "OK" : "KO"); } } public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(holder::init); holder.display(); } }
On voudrait un mécanisme qui :
En Java, c'est la paire de méthodes wait()
et notify()
du lock qui permet de faire ça.
public class Holder { private int value; private boolean done; private final Object lock = new Object(); private void init() { synchronized (lock) { value = 42; done = true; lock.notify(); // réveille un thread s'il est en pause (sinon, ne fait rien) } } private void display() throws InterruptedException { synchronized (lock) { while (!done) { lock.wait(); // demande de mettre le thread en pause } System.out.println(value == 42 ? "OK" : "KO"); } } public static void main(String[] args) throws InterruptedException { var holder = new Holder(); Thread.ofPlatform().start(holder::init); holder.display(); } }
Pourquoi garder le booléen ? Pourquoi le tester dans une boucle ?
lock.wait()
Lorsque la méthode est appelée :
lock
,
Lorsque le thread est réveillé (après l'appel à wait
) :
lock
(quand il est disponible) ;
Les appels à lock.wait()
doivent donc obligatoirement être faits à l’intérieur d'un bloc synchronized(lock)
!
notify()
et notifyAll()
lock.notify()
: demande le réveil d'un thread en attente sur le lock
(à cause d'un appel lock.wait()
).
notify()
est perdu et il ne se passe rien.
lock.notifyAll()
: demande le réveil de touts les threads en attente sur le lock
(à cause d'un appel à lock.wait()
).
synchronized
.
Les appels à lock.notify()
ou lock.notifyAll()
doivent être faits à l’intérieur d'un bloc synchronized(lock)
(nous avons allons voir pourquoi rapidement).
Comment un thread peut être réveillé d'un appel à lock.wait()
?
lock.notify()
ou lock.notifyAll()
.
interrupt()
(dans ce cas lock.wait()
lève InterruptedException
, on verra ça dans un prochain cours).
public class Holder { private int value; private boolean done; private final Object lock = new Object(); private void init() { synchronized (lock) { value = 42; done = true; lock.notify(); } } private void display() throws InterruptedException { synchronized (lock) { while (!done) { lock.wait(); } System.out.println(value == 42 ? "OK" : "KO"); } } public static void main(String[] args) throws InterruptedException { var holder = new Holder(); Thread.ofPlatform().start(holder::init); holder.display(); } }
Supposons qu'on enlève le booléen done
.
Si l'autre thread exécute le notify()
avant que le thread main
soit dans le wait()
,
le notify()
sera perdu... et le wait()
attendra indéfiniment !
Donc l'appel à lock.notify()
est fait à l’intérieur d'un bloc synchronized(lock)
qui met à jour la condition associée au wait()
.
Pour éviter les notifications perdues... et donc une attente infinie dans un wait()
:
notify
.
wait()
ou pas.
synchronized
contenant le notify
associé (sinon, on retrouve tous les problèmes liés à la non atomicité des instructions).
while
et pas un if
?public class Holder { private int value; private boolean done; private final Object lock = new Object(); private void init() { synchronized (lock) { value = 42; done = true; lock.notify(); } } private void display() throws InterruptedException { synchronized (lock) { while (!done) { lock.wait(); } System.out.println(value == 42 ? "OK" : "KO"); } } public static void main(String[] args) throws InterruptedException { var holder = new Holder(); Thread.ofPlatform().start(holder::init); holder.display(); } }
Pour se protéger du spurious wake-up !
Lorsque l'on sort du wait()
, on doit à nouveau tester la condition, pour être sûr que c'est bien la raison qui nous a fait sortir du wait()
.
Un wait()
n'est jamais tout seul !
L'appel à wait()
doit toujours :
synchronized
;while
;notify
.L'appel à notify()
est toujours :
synchronized
wait()
) ;wait()
.