ReentrantLock

File d'attente thread-safe (come-back)

On reprend l'exercice du TP sur les signaux où l'on veut créer une file d'attente thread-safe.

Dans un premier temps, vous avez réalisé une file d'attente thread-safe non-bornée appelée UnboundedSafeQueue<V> qui offrait deux méthodes:

Vous avez dû obtenir du code qui ressemble à :

public class UnboundedSafeQueue<V> {
  private final ArrayDeque<V> fifo = new ArrayDeque<>();
  private final Object lock = new Object();

  public void add(V value) {
    synchronized (lock) {
      fifo.add(value);
      lock.notify();
    }
  }

  public V take() throws InterruptedException {
    synchronized (lock) {
      while (fifo.isEmpty()) {
        lock.wait();
      }
      return fifo.remove();
    }
  }
}

Ré-écrire (refactor) cette classe pour qu'elle utilise l'API des ReentrantLock.

Penser à tester que tout s'affiche correctement.

Ensuite, vous avez réalisé une file d'attente thread-safe de taille bornée BoundedSafeQueue<V>. La capacité maximale est fixée à la création de la file et la taille de la file ne devra jamais dépasser cette capacité. La méthode add n'est donc plus adaptée car elle ne prend pas en compte la capacité maximale. On la remplace par une méthode put(V value) qui ajoute value s'il y a la place et sinon attend jusqu'à ce qu'une place se libère.

Vous avez dû obtenir du code qui ressemble à :

public class BoundedSafeQueue<V> {
  private final ArrayDeque<V> fifo = new ArrayDeque<>();
  private final int capacity;
  private final Object lock = new Object();
  
  public BoundedSafeQueue(int capacity) {
    if (capacity <= 0) {
      throw new IllegalArgumentException();
    }
    this.capacity = capacity;
  }

  public void add(V value) throws InterruptedException {
    synchronized (lock) {
      while (fifo.size() == capacity) {
        lock.wait();
      }
      fifo.add(value);
      lock.notifyAll();
    }
  }

  public V take() throws InterruptedException {
    synchronized (lock) {
      while (fifo.isEmpty()) {
        lock.wait();
      }
      lock.notifyAll();
      return fifo.remove();
    }
  }

Pourquoi utilise-t-on notifyAll et non pas notify ici ?

Récrire (refactor) cette classe pour qu'elle utilise l'API des ReentrantLock

L'API des ReentrantLock offre un moyen d'éviter d'utiliser signalAll dans ce contexte. Modifier le code en conséquence.

Vérifier que l'on observe le comportement attendu quand on a plus de 5 threads (10, 50, ...) qui utilisent la méthode add.

Permis de synchronisation (Optionnel)


On souhaite écrire une classe thread-safe Sync qui possède deux méthodes safe et inSafe. La méthode safe garantit qu'un seul thread à la fois peut exécuter le code d'un Supplier (et ce, quel que soit le Supplier). La méthode inSafe, renvoie vrai si un thread (n'importe lequel) est en train d'exécuter un Supplier avec la méthode safe en même temps.

public class Sync<V> {
  public boolean inSafe() {
    // TODO
  }
  
  public V safe(Supplier<? extends V> supplier) throws InterruptedException {
    return supplier.get();  // TODO
  }
}

Écrire le code d'une classe Counter ayant une méthode count qui renvoie une valeur et incrémente celle-ci à chaque appel. Afin d'être thread-safe, votre implantation devra utiliser la classe Sync et supposant qu'il existe déjà une implantation. La classe Counter devra aussi avoir une méthode isSomeOneCounting qui renvoie vrai si un thread est en train d’utiliser sa méthode count().

Écrire également le code du main de la classe Counter qui créé un compteur et deux threads qui affichent chacun les valeurs renvoyées par le compteur dans une boucle infinie. Ajouter un thread qui affiche à intervalles réguliers si un autre thread est en train de compter ou pas.

Écrire le code de la classe Sync en utilisant des blocs synchronized.

Écrire le code de la classe Sync en utilisant les verrous du package java.util.concurrent.

On souhaite maintenant écrire une classe thread-safe PermitSync qui possède une méthode safe. La méthode safe garantit qu'un nombre de threads inférieur ou égal à permits peuvent exécuter en même temps le code d'un Supplier de leur choix (et ce, quels que soient les Supplier choisis).

public class PermitSync<V> {
  public PermitSync(int permits) {
    // TODO 
  }
  
  public V safe(Supplier<? extends V> supplier) {
    return supplier.get();  // TODO
  }
}
  

Écrire le code de la classe PermitSync en utilisant des blocs synchronized.

Écrire le code de la classe PermitSync en utilisant les verrous du package java.util.concurrent.


Recherche de nombres premiers

Dans cet exercice, on veut écrire un programme qui lance 5 threads qui cherchent en boucle des grands nombres premiers. Le thread main attend que 10 nombres premiers aient été trouvés, affiche leur somme et s'arrête.

Le code des threads ressemblera au code suivant :

for (;;) {
    long nb = 1_000_000_000L + ThreadLocalRandom.current().nextLong(1_000_000_000L);
    if (isPrime(nb)) {
        ...
    }
}

avec

public static boolean isPrime(long l) {
    if (l <= 1) {
        return false;
    }
    for (long i = 2L; i <= l / 2; i++) {
        if (l % i == 0) {
            return false;
        }
    }
    return true;
}

On souhaite réaliser cette tâche avec les contraintes suivantes :

Décrire le contrat (c'est à dire quelles sont les méthodes qu'elle doit fournir et ce qu'elles font précisément) d'une classe MyThreadSafeClass permettant de réaliser cette tâche.

Écrire la classe MyThreadSafeClass, en utlisant l'API des ReentrantLock.

Écrire la méthode main d'une classe SumFirstTenPrimes qui réalise la tâche demandée en utilisant MyThreadSafeClass.

Sémaphores

Dans cet exercice, on cherche à écrire une classe thread-safe Semaphore. Un objet de cette classe (un sémaphore) contient un certain nombre de permis d’exécution (le nombre initial est fixé à la construction) et permet d'en acquérir et d'en rendre.

La classe ne stocke pas réellement des permis (il n'y a pas d'objet "permis") mais simplement le nombre de permis disponibles dans le sémaphore.

Le nombre initial de permis est passé en argument du constructeur de la classe. La classe fournit 3 méthodes :

En utilisant l'API des ReentrantLock, écrire une classe Semaphore thread-safe et qui respecte le contrat ci-dessus.

Remarque : ce n'est pas une bonne idée, en termes de lisibilité, d'utliser tryAquire dans le code de la méthode aquire.

Rajouter à votre classe Semaphore une méthode main qui crée un sémaphore contenant 5 permis puis démarre 10 threads. Chaque thread attend de pouvoir prendre un permis. Puis, après l'avoir pris, il attend 1 seconde et rend le permis. Chaque thread affichera un message (avec son numéro) après avoir acquis et après avoir rendu un permis.

On veut étendre les fonctionnalités de notre classe en rajoutant la possibilité de savoir combien de threads sont bloqués en attente d'un permis d'exécution. Pour cela, on va rajouter une méthode int waitingForPermits() qui renvoie le nombre de threads qui sont en attente dans acquire.

Recopier votre classe Semaphore dans une classe SemaphoreClosable et rajouter la méthode waitingForPermits décrite ci-dessus. Modifier le code pour que le main attende 1 seconde après le démarrage des threads et affiche la valeur renvoyée par waitingForPermits.

On veut maintenant ajouter la possibilité de fermer le sémaphore. Pour cela on va rajouter une méthode void close(). La fermeture du sémaphore devra avoir le comportement suivant :

Rajouter la méthode close décrite ci-dessus à votre classe SemaphoreClosable.

On veut modifier le main pour qu'après avoir démarré les threads, il attende 10 secondes et ferme le sémaphore s'il n'y a aucun thread en attente dans un acquire. Quelqu'un propose le code ci-dessous :

   var semaphore = new Semaphore(...);
   ...
   if (semaphore.waitingForPermits() == 0){
      semaphore.close();
   }

Quel est le problème avec le code proposé ? Comment faire évoluer la classe SemaphoreClosable pour pouvoir obtenir cette fonctionnalité ? Répondez en commentaire dans la classe SemaphoreClosable.