(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().