:: Enseignements :: Master :: M1 :: 2019-2020 :: Java Avancé ::
[LOGO]

Volatile et opérations atomiques


Exercice 1 - A vos chronometres

On cherche à savoir combien d'itérations d'une boucle on peut faire en 100 millisecondes. Un de vos collègues a produit le code suivant:

  1. Sans exécuter le code, que fait ce programme ?
  2. Vérifier, en exécutant le programme (plusieurs fois), si vous avez vu juste.
  3. Comment doit-on corriger le problème ?
    Modifier la classe Bogus en conséquence.
  4. On cherche maintenant a accélérer le code de Bogus en utilisant le mot clé volatile au lieu des blocs synchronized.
    Créer une classe BogusVolatile qui n'utilise pas de bloc synchronized.
    Comment appelle-t-on les implantations qui n'ont ni blocs synchronized, ni lock ?

Exercice 2 - SpinLock

On cherche à écrire un simple lock non-réentrant en utilisant les volatiles et la méthode compareAndSet de la classe VarHandle.

  1. Rappeler ce que "réentrant" veux dire.
  2. 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.
  3. 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 ?
  4. 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.

Exercice 3 - Generateur pseudo-aléatoire lock-free

On souhaite modifier la classe RandomNumberGenerator pour la rendre thread-safe sans utiliser ni section critique ni verrou (lock-free donc).

  1. Expliquer comment fonctionne un générateur pseudo-aléatoire et pourquoi l'implantation ci-dessous n'est pas thread-safe.
  2. Utiliser la classe AtomicLong et la méthode compareAndSet pour obtenir une implantation lock-free du générateur pseudo-aléatoire.
  3. Depuis le jdk 1.8, la classe AtomicLong possède une méthode updateAndGet, comment peut-on l'utiliser ici ? Modifiez votre code en conséquence.
  4. Un des inconvénients des champs "atomiques" est qu'ils alourdissent l'allocation nécessaire pour chaque objet. En effet, pour utiliser un long pour chaque objet générateur, il faut allouer un AtomicLong qui permettra lui-même d'accéder de manière atomique à la valeur d'un long, chaque accès nécessitant lui-même une indirection...
    Faites une nouvelle implantation du générateur pseudo-aléatoire qui utilise la classe VarHandle. Un VarHandle ne nécessite qu'une seule instance (static) pour mettre à jour de manière atomique le champ volatile long de n'importe lequel des objets générateur de cette classe.

Exercice 4 - Reentrant spin lock [Optionel]

Pour les braves, 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 == 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 est 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 lève 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);
  }
}
   

  1. Écrire la classe ReentrantSpinLock et vérifier que le main fonctionne.
  2. 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/ecriture des champs aux alentours.
    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.