(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; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { holder.value = 42; }); // dans le main while (!holder.value == 0) { // 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; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { holder.value = 42; }); // le code peut être optimisé pour utiliser une variable locale... var tmp = holder.value while (tmp == 0) { // 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; public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(() -> { holder.value = 42; }); while (holder.value == 0) { // 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 final Object lock = new Object(); void init() { synchronized (lock) { value = 42; } } int hold() { for (;;) { synchronized (lock) { if (value != 0) { return value; } } } } public static void main(String[] args) { var holder = new Holder(); Thread.ofPlatform().start(holder::init); var value = holder.hold() System.out.println(value == 42 ? "OK" : "KO"); } }
Ça ne résout pas le problème de l'attente active !
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 final Object lock = new Object(); void init() { synchronized (lock) { value = 42; lock.notify(); // réveille un thread s'il est en pause (sinon, ne fait rien) } } int hold() throws InterruptedException { synchronized (lock) { while (value == 0) { lock.wait(); // demande de mettre le thread en pause } retrun value; } } public static void main(String[] args) throws InterruptedException { var holder = new Holder(); Thread.ofPlatform().start(holder::init); var value = holder.hold() System.out.println(value == 42 ? "OK" : "KO"); } }
Pourquoi garder le test ? 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 aussi ê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 final Object lock = new Object(); void init() { synchronized (lock) { value = 42; lock.notify(); } } int hold() throws InterruptedException { synchronized (lock) { while (value == 0) { lock.wait(); } retrun value; } } public static void main(String[] args) throws InterruptedException { var holder = new Holder(); Thread.ofPlatform().start(holder::init); var value = holder.hold() System.out.println(value == 42 ? "OK" : "KO"); } }
Supposons qu'on enlève le test.
Si l'autre thread exécute 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 l'attente infinie dans wait()
:
notify
.
wait()
ou pas.
synchronized
contenant le notify
associé (sinon, on retrouve tous les problèmes de data-race habituels).
while
et pas un if
?public class Holder { private int value; private final Object lock = new Object(); void init() { synchronized (lock) { value = 42; lock.notify(); } } int hold() throws InterruptedException { synchronized (lock) { while (value == 0) { lock.wait(); } retrun value; } } public static void main(String[] args) throws InterruptedException { var holder = new Holder(); Thread.ofPlatform().start(holder::init); var value = holder.hold() System.out.println(value == 42 ? "OK" : "KO"); } }
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()
.