public class PingPong { private final Object lock1 = new Object(); private final Object lock2 = new Object(); public void ping() { synchronized (lock1) { synchronized (lock2) { System.out.println("ping"); } } } public void pong() { synchronized (lock2) { synchronized (lock1) { System.out.println("pong"); } } } public static void main(String[] args) throws InterruptedException { var pingpong = new PingPong(); Thread.ofPlatform().start(() -> { for (;;) { pingpong.ping(); } }); for (;;) { pingpong.pong(); } } }
Quel affichage obtient-on ? Parfois des ping et des pong ... parfois que des ping ou que des pong ... ou rien ! Mais presque sûrement, ça bloque!
ping()
prend le verrou du lock1
,
lock2
).
pong()
prend le verrou du lock2
,
lock1
).
lock2
... mais ne peut pas tant que le thread "pong" ne le lâche pas...
lock1
. On est donc bloqués!
Si plusieurs threads (au moins 2) prennent plusieurs verrous (au moins 2), dans un ordre différent, ...
... il risque d'y avoir un problème d'interblocage.
Deux solutions :
Il existe une 3e solution : la programmation lock-free (qui utilise des champs volatile
et des opérations atomiques spéciales), nous verrons cela en fin de semestre.
Si l'on connaît tous les locks du programme,
alors le programme ne peut pas atteindre un état d'interblocage.
Pourquoi ?Pour simplifier
On suppose par l'absurde qu'il y a un interblocage et on va chercher une contradiction.
Le thread t1 attend le lock i qui est pris par t2.
Le thread t2 attend le lock j qui est pris par t1.
Comme lock i est déja pris par t2, on a lock i < lock j.
Comme lock j est déja pris par t1, on a lock j < lock i.
On aurait alors lock i < lock j ET lock j < lock i... on a une contradiction!
public class DeadLock { private final Object lock1 = new Object(); private final Object lock2 = new Object(); private boolean finished; public void process() throws InterruptedException { synchronized (lock1) { synchronized (lock2) { while (!finished) { lock2.wait(); } } } System.out.println("Finished !"); } public void stop(){ synchronized (lock1) { synchronized (lock2) { finished = true; lock2.notify(); } } } public static void main(String[] args) throws InterruptedException { var deadLock = new DeadLock(); Thread.ofPlatform().start(()-> { deadLock.stop(); }); deadLock.process(); } }
Attention, lock2.wait()
rend le lock2
mais pas le lock1
!
N'utilisez pas plusieurs locks dans la même classe !
public class A { private final int value; private static A a; public A(int value) { this.value = value; } public static void main(String[] args) { Thread.ofPlatform().start(() -> { if (a != null) { System.out.println(a.value); } // affiche 7 ou rien }); a = new A(7); } }
Publication : un champ déclaré final
est visible avec sa valeur d'initialisation par tous les threads après la fin du constructeur !
public class A { privatefinalint value; private static A a; public A(int value) { this.value = value; } public static void main(String[] args) { Thread.ofPlatform().start(() -> { if (a != null) { System.out.println(a.value); } // affiche 7 ou 0 !!! ou rien }); a = new A(7); } }
Si on oublie le final
, on peut voir la valeur par défaut.
Ici, c'est value = 0
.
public class A { private final int value; private static A a; A(int value) { this.value = value; A.a = this; // quelle drôle d'idée... } public static void main(String[] args) { Thread.ofPlatform().start(() -> { if (a != null) { System.out.println(a.value); } // affiche 7 ou 0 !!! ou rien }); new A(7); } }
Si on publie this
avant la fin du constructeur, il y a un risque aussi. Ça peut arriver, pour faire un singleton, mais là encore, ça n'est pas l'objet de ce cours...
Pour faire simple : ne le faites pas !
Enfin, on pourrait avoir envie de démarrer un thread dans un constructeur.
Dans ce cas, le thread aura très probablement accès à this
(capturé dans l'environement du Runnable
) avant qu'il soit publié...
Pour faire simple : ne le faites pas !
Si on a vraiment besoin de démarrer un thread avec la classe, on peut le démarrer dans une méthode start
qui sera appelée après la construction de l'objet.
final
?On utilise un bloc synchronized
dans le constructeur.
Il y a une autre façon de faire, en utilisant les champs volatile
, mais on verra ça plus tard.
Les initialisations statiques (directes ou dans un bloc statique) sont visibles par touts les threads qui utilisent la classe.
Remarque : la bloc statique est executé avant l'accès à une variable statique, à une méthode statique ou à un constructeur.
class A { private static Object o = new Object(); private static String[] tab = new String[100]; static { Arrays.setAll(tab, __ -> "foo"); } public static void main(String[] args) { Thread.ofPlatform().start(() -> { System.out.println(A.o + " " + Arrays.toString(A.tab)); // pas de null }); System.out.println(A.o + " " + Arrays.toString(A.tab)); // pas de null } }
Donc pas de problème de publication avec les intialisations statiques.
Dans un constructeur,
this
Dans un constructeur, pas besoin de synchronisation :
sinon il faut utiliser la synchronisation !