:: Enseignements :: ESIPE :: E4INFO :: 2019-2020 :: 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 appelera
SplinLock
en utilisant les volatiles et la méthode
compareAndSet de la classe
VarHandle.
-
Rappeler ce que "réentrant" veux dire.
-
Expliquer, pour le code ci-dessous, quel est le comportement que l'on attend si la classe est thread-safe.
Puis expliquer pourquoi le code du Runnable n'est pas thread-safe
(même si on déclare counter volatile).
Vérifier vos affirmations en exécutant le main.
-
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 champs booléen de faux à vrai. Redonner le lock revient à assigner le champ à faux.
Que doit-on faire si on arrive pas à acquérir le lock ? Quel est le problème ?
Pourquoi utiliser Thread.onSpinWait permet de résoudre le problème ?
Sachant que les CPUs modernes ont un pipeline, à votre avis, à quoi sert Thread.onSpinWait ?
-
Implanter les méthodes lock et unlock 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);
}
}
-
Ecrire 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 lectere/ecriture volatile sur lock a aussi des effets
sur la lecture/ecriture des champs au alentour.
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/ecriture 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 ne marche pas ?
public class Utils {
private static Path HOME;
public static Path getHome() {
if (HOME == null) {
return HOME = Path.of(System.getenv("HOME"));
}
return HOME;
}
}
-
Peux-t'on dire que le code ci-dessus n'est pas thread-safe ?
-
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 fournient 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