:: Enseignements :: ESIPE :: E4INFO :: 2022-2023 :: Collections Concurrentes ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
Memory Model, Publication et Lock
|
Memory Model, Opération atomique, CompareAndSet, Réimplantation de locks
Exercice 1 - SpinLock pas réentrant
On cherche à écrire un simple lock non-réentrant que l'on appellera
SpinLock
en utilisant les volatiles et la méthode
compareAndSet de la classe
VarHandle.
-
Rappeler ce que "réentrant" veux dire.
-
Nous allons pour cela écrire une version thread-safe de la classe suivante
package fr.umlv.conc;
public class SpinLock {
public void lock() {
// TODO
}
public void unlock() {
// TODO
}
public static void main(String[] args) throws InterruptedException {
var runnable = new Runnable() {
private int counter;
private final SpinLock spinLock = new SpinLock();
@Override
public void run() {
for(int i = 0; i < 1_000_000; i++) {
spinLock.lock();
try {
counter++;
} finally {
spinLock.unlock();
}
}
}
};
var t1 = new Thread(runnable);
var t2 = new Thread(runnable);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter " + runnable.counter);
}
}
Pour coder le lock, l'idée est d'utiliser un champ boolean pour représenter le jeton que doit prendre un thread pour rentrer dans le lock.
Acquérir le lock revient à passer le champ booléen de faux à vrai. Redonner le lock revient à assigner le champ à faux.
Écrire le code correspondant en faisant attention à la déclaration du champ contenant le boolean.
Pour l'instant, nous allons va tenter d'acquérir le lock quoi qu'il arrive.
-
Le problème du code que l'on vient d'écrire est qu'il fait une attente active.
Expliquer pourquoi on ne peut pas faire attendre un thread qui n'a pas le lock en utilisant la méthode wait ?
-
Il existe une méthode Thread.onSpinWait. Que fait cette méthode exactement, et peut-on l'utiliser ici ?
-
Modifier votre code et vérifier en exécutant le main que le code du Runnable est désormais thread-safe.
-
Ajouter une méthode tryLock dans SpinLock qui a le même comportement que la méthode
tryLock de l'interface java.util.concurrent.locks.Lock.
Après avoir mis le code du main en commentaire, ajouter un test vérifiant le fonctionnement de tryLock.
Exercice 2 - SpinLock Réentrant
On souhaite maintenant écrire un lock réentrant, on propose pour cela l'algorithme suivant
public class ReentrantSpinLock {
private volatile int lock;
private volatile Thread ownerThread;
public void lock() {
// idée de l'algo
// on récupère la thread courante
// si lock est == à 0, on utilise un CAS pour le mettre à 1 et
// on sauvegarde la thread qui possède le lock dans ownerThread.
// sinon on regarde si la thread courante n'est pas ownerThread,
// si oui alors on incrémente lock.
//
// et il faut une boucle pour retenter le CAS après avoir appelé onSpinWait()
}
public void unlock() {
// idée de l'algo
// si la thread courante est != ownerThread, on pète une exception
// si lock == 1, on remet ownerThread à null
// on décrémente lock
}
public static void main(String[] args) throws InterruptedException {
var runnable = new Runnable() {
private int counter;
private final ReentrantSpinLock spinLock = new ReentrantSpinLock();
@Override
public void run() {
for(var i = 0; i < 1_000_000; i++) {
spinLock.lock();
try {
spinLock.lock();
try {
counter++;
} finally {
spinLock.unlock();
}
} finally {
spinLock.unlock();
}
}
}
};
var t1 = new Thread(runnable);
var t2 = new Thread(runnable);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("counter " + runnable.counter);
}
}
-
Écrire la classe ReentrantSpinLock et vérifier que le main fonctionne.
-
En fait, on peut rendre le code un peu plus efficace en ne déclarant pas ownerThread volatile
et en profitant du fait que l'effet d'une lecture/écriture volatile sur lock a aussi des effets
sur la lecture/écriture des autres champs.
Modifier votre code et vérifier que le main fonctionne toujours.
Note: la lecture d'un champ par une même thread ne nécessite pas de lecture/écriture volatile.
Exercice 3 - Double-Checked Locking
Le Double-Checked Locking est un design pattern de concurrence hérité du C++
qui consiste à essayer d'initialiser un singleton de façon paresseuse (lazy) et thread-safe.
Ce design pattern ne sert à rien en Java (on va voir pourquoi) mais c'est un exercice comme un autre.
-
Pourquoi le code suivant n'est pas thread-safe ?
public class Utils {
private static Path HOME;
public static Path getHome() {
if (HOME == null) {
return HOME = Path.of(System.getenv("HOME"));
}
return HOME;
}
}
-
Pourquoi le code suivant ne marche pas non plus ?
Indice: pensez au problème de publication !
Comment peut-on corriger le problème ?
public class Utils {
private static Path HOME;
public static Path getHome() {
var home = HOME;
if (home == null) {
synchronized(Utils.class) {
home = HOME;
if (home == null) {
return HOME = Path.of(System.getenv("HOME"));
}
}
}
return home;
}
}
-
Il existe une autre façon de corriger le problème en utilisant les méthodes
getAcquire et setRelease de la classe
VarHandle
qui fournisse un accès relaxé à la mémoire.
Quelles sont les garanties fournies par getAcquire et setRelease
et en quoi ces garanties sont moins forte qu'un accès volatile ?
En quoi ces garanties sont suffisantes dans notre cas ?
Modifier le code ci-dessous (en commentant l'ancien) en introduisant les méthodes getAcquire et setRelease:
public class Utils {
private static Path HOME;
public static Path getHome() {
var home = HOME; // TODO ??
if (home == null) {
synchronized(Utils.class) {
home = HOME; // TODO ??
if (home == null) {
return HOME = Path.of(System.getenv("HOME")); // TODO ??
}
}
}
return home;
}
}
-
En fait, il y a une façon beaucoup plus simple de résoudre le problème
sans utiliser le pattern Double-checked locking et en utilisant le pattern
initialization on demand holder.
Modifier votre code (en commentant l'ancien) pour utiliser ce pattern.
Pourquoi ce pattern est plus efficace ?
© Université de Marne-la-Vallée