Synchronized

Les effets du bloc synchronized

On considère le code suivant. Répondez dans un fichier texte nommé exo1.txt.

public class MyClass {
   private String first;
   private String second;
   private final Object lock = new Object();

   public MyClass(String first, String second) {
      this.first = first;
      this.second = second;
   }

   public void set(String value1, String value2) {
      synchronized (lock) {
         first = value1;  
         second = value2; 
      }
   }

   public void setCheckFirst(String value1, String value2) {
      if (value1 != null) {
         synchronized (lock) {
            first = value1;
         }
      }
      synchronized (lock) {
         second = value2;
      }
   }

   @Override
   public String toString() {
      synchronized (lock) {
         return first + " + " + second;
      }
   }

   public static void main(String[] args) throws InterruptedException { 
      var quizz = new MyClass("mouse", "duck");

      var thread1 = Thread.ofPlatform().daemon().start(() -> {
         for (;;) {
            quizz.set("cat", "dog"); 
            System.out.println("1. " + quizz);
         }
      });

      var thread2 = Thread.ofPlatform().start(() -> {
         quizz.setCheckFirst("bird", "fish");
         System.out.println("2. " + quizz);
      });

      thread2.join();
      //thread1.join();

      quizz.set("mouse", "duck");        
      System.out.println("3. " + quizz); 
   }
}

Pour chaque question, indiquez si vous pensez que l’affirmation est correcte ou si vous pensez qu’elle est fausse. Lors de l’exécution de MyClass, ...

  1. ... le thread main peut être dé-schédulé entre les deux affectations dans l'appel au constructeur (line 37).
  2. ... le thread1 peut être dé-schédulé entre les deux affectations dans son appel à la méthode set() (line 41).
  3. ... le thread1 ne redonne pas le jeton du lock entre les deux affectations dans l'appel à la méthode set() (line 41).
  4. ... l'ordre des affectations dans le code de set() peut être inversé (lignes 13 et 14).
  5. ... l'ordre des deux dernières instructions du main peut être inversé (lignes 54 et 55).
  6. ... on peut avoir l'affichage "1. mouse + duck".
  7. ... on peut avoir l'affichage "1. bird + fish".
  8. ... on peut avoir l'affichage "1. cat + fish".
  9. ... on peut avoir l'affichage "1. bird + dog".
  10. ... on peut avoir l'affichage "2. mouse + duck".
  11. ... on peut avoir l'affichage "2. bird + duck".
  12. ... l'affichage "3. ..." peut être autre chose que "3. mouse + duck".
  13. ... le programme peut ne pas s'arreter.
  14. ... si on dé-commente l'instruction thread1.join(); le programme ne s'arrête pas.

Tableau d'honneur

Une école possède un tableau d'honneur (où l'on met le nom et le prénom de l'élève le plus méritant) qui peut être mis à jour de façon informatique. Il n'en faut pas plus pour que Mickey Mouse et Donald Duck, nos deux apprentis hackers, écrivent un petit programme qui met à jour automatiquement le tableau d'honneur avec leurs noms : HonorBoard.java.

Malheureusement, la classe HonorBoard n'est pas thread-safe, donc le code ne se comporte pas comme prévu.

Expliquer pourquoi la classe HonorBoard n'est pas thread-safe.
Si vous ne voyez pas, faites un grep "Mickey Duck" sur la sortie du programme et donner un scénario pouvant mener à cet affichage.

Rappel général : un test qui plante indique un problème, un test qui ne plante pas n'indique rien du tout.

Modifier le code de la classe HonorBoard pour la rendre thread-safe .
Vérifier avec grep sur la sortie comme précédemment (pendant plusieurs minutes).

Maintenant que votre classe est thread-safe, peut-on remplacer la ligne :

System.out.println(board);
par la ligne :
System.out.println(board.firstName() + ' ' + board.lastName());
avec les deux accesseurs définis comme d'habitude et en utilisant des bloc synchronized ?
  public String firstName() {
    synchronized (lock) {
      return firstName;
    }
  }

  public String lastName() {
    synchronized (lock) {
      return lastName;
    }
  }
Vérifier en exécutant le code. La classe HonorBoard est-elle toujours thread-safe ?

Optionnel - When things pile up (come back)

On reprend une dernière fois le code qui entrelace les accès à une ArrayList et cette fois-ci on ne fixe pas la capacité initiale de la liste :

public class HelloListBug {
  public static void main(String[] args) throws InterruptedException {
    var nbThreads = 4;
    var threads = new Thread[nbThreads]; 
    
    var list = new ArrayList<Integer>();

    IntStream.range(0, nbThreads).forEach(j -> {
      Runnable runnable = () -> {
        for (var i = 0; i < 5000; i++) {
          list.add(i);
        }
      };

      threads[j] = Thread.ofPlatform().start(runnable);
    });

    for (Thread thread : threads) {
      thread.join();
    }

    System.out.println("taille de la liste:" + list.size());
  }
}

Exécuter le programme plusieurs fois. Quel est le nouveau comportement observé ? Expliquer quel est le problème. Là encore, il faut regarder le code de la méthode ArrayList.add.

Méthode bloquante ?

Dans cet exercice, on souhaite écrire une classe RendezVous thread-safe qui permet de réaliser le passage d'une valeur entre des threads. C'est une première tentative qui ne fonctionnera pas de réaliser une méthode bloquante. Nous verrons au prochain cours comment résoudre ce problème de façon satisfaisante.

Dans le programme FindPrime.java, plusieurs threads sont démarrés et tirent des grands nombres au hasard jusqu'à en trouver un qui soit premier. Le principe est que le main attend jusqu'à ce que l'un des threads ait trouvé un nombre premier. Pour réaliser le passage de la valeur entre les threads qui cherchent des nombres premiers et le main, nous allons écrire une classe RendezVous.

  public static void main(String[] args) {
    var nbThreads = 4;
    var rdv = new StupidRendezVous();

    IntStream.range(0, nbThreads).forEach(i -> {
      Thread.ofPlatform().daemon().start(() -> {
        for (;;) {
          var nb = BIG_LONG + ThreadLocalRandom.current().nextLong(BIG_LONG);
          if (isPrime(nb)) {
            rdv.set(nb);
            System.out.println("A prime number was found in thread " + i);
            return;
          }
        }
      });
    });

    try {
      System.out.println("I found a large prime number : " + rdv.get());
    } catch (InterruptedException e) {
      throw new AssertionError();
    }
  }

La classe RendezVous est sensée être une classe thread-safe qui offre un méthode set permettant de proposer une valeur et une méthode get qui ''bloque'' jusqu'à ce qu'une valeur ait été proposée et la renvoie lorsque c'est le cas.

On commence avec une très mauvaise tentative dans la classe StupidRendezVous.java :

/**
 * Note: this code does several stupid things !
 */
package fr.uge.concurrence;

import java.util.Objects;

public class StupidRendezVous<V> {
  private V value;

  public void set(V value) {
    Objects.requireNonNull(value);
    this.value = value;
  }

  public V get() throws InterruptedException {
    while (value == null) {
      Thread.sleep(1); // then comment this line !
    }
    return value;
  }
}

Que se passe-t-il lorsqu'on exécute ce code ?

Commenter l'instruction Thread.sleep(1) dans la méthode get puis ré-exécuter le code. Que se passe-t-il ? Expliquer où est le bug ?

Écrire une classe thread-safe RendezVous sur le même principe que la classe StupidRendezVous mais qui fonctionne correctement, que l'instruction Thread.sleep(1) soit commentée ou non.

Regarder l'utilisation du CPU par votre programme avec la commande top. Votre code fait de l'attente active ce qui n'est pas une solution acceptable, mais vous n'avez pas les outils pour corriger cela pour l'instant. Nous verrons au prochain cours comment réaliser une méthode bloquante sans faire de l'attente active.

L'attente active consiste à périodiquement vérifier l'état d'un objet jusqu'à observer le changement d'état désiré. Cela produit une boucle qui consomme énormément de ressource CPU. Il faut absolument éviter ce comportement.

Maximum Thread-safe

Dans cet exercice, on va écrire un programme MaximumRace.java qui démarre 4 threads qui tirent au hasard et affichent 10 valeurs entières, en attendant une seconde entre chaque tirage. On voudra que pendant ce temps là, le main puisse afficher à intervalles de temps réguliers quelle est le maximum courant des valeurs proposées par l'ensemble des threads.

Voici un extrait du programme :

  private static void checkedSleep(int millis) {
    try {
      Thread.sleep(millis);
    } catch (InterruptedException e) {
      throw new AssertionError();
    }
  }

  public static void main(String[] args) {
    var nbThreads = 4;
    var threads = new Thread[nbThreads];

    for (var j = 0; j < nbThreads; j++) {
      threads[j] = Thread.ofPlatform().start(() -> {
        for (var i = 0; i < 10; i++) {
          checkedSleep(1000);
          var value = ThreadLocalRandom.current().nextInt(SharedInformation.MAX_VALUE);
          System.out.println(Thread.currentThread() + " a tiré " + value);
        }
      });
    }

    for (int i = 0; i < 10; i++) {
      checkedSleep(1000);
      System.out.println("Max courant : ");
    }

    ...
  }

Quatre threads sont démarrés. Chacun attend une seconde avant de tirer une valeur aléatoire dans l'intervalle [0, MAX_VALUE[ : À ce moment là, le thread affiche la valeur et il fait ça 10 fois. MAX_VALUE = 10_000 est une constante de la classe SharedInformation que nous allons écrire dans la suite.
Pendant ce temps, toutes les secondes, le main affiche quel est le maximum courant parmi toutes les valeurs tirées par les threads. Après 10 affichages du maximum courant, le main attend que les autres threads aient finit et affiche le maximum final avant de terminer.

Pourquoi a-t-on besoin d'une classe thread-safe pour réaliser ce programme ? Quelles doivent être les méthodes fournies par cette classe (on dit que c'est le "contrat" de la classe) ?

Écrire la classe thread-safe SharedInformation correspondante avec les en-têtes des ses méthodes (mais pas le code, on le fera ensuite).

Complétez le code du main, pour qu'il réalise le travail demandé, en utilisant SharedInformation.

Écrire le code des méthodes de SharedInformation de façon à ce qu'elle soit thread-safe et afin que le code du main fonctionne. Son affichage devrait ressembler à ça :

Thread[#20,Thread-0,5,main] a tiré 3431
Thread[#23,Thread-3,5,main] a tiré 1889
Thread[#22,Thread-2,5,main] a tiré 3
Thread[#21,Thread-1,5,main] a tiré 5818
Max courant : 5818
Thread[#22,Thread-2,5,main] a tiré 2621
Thread[#21,Thread-1,5,main] a tiré 4143
Thread[#20,Thread-0,5,main] a tiré 4022
Thread[#23,Thread-3,5,main] a tiré 666
Max courant : 5818
Thread[#20,Thread-0,5,main] a tiré 2587
Thread[#22,Thread-2,5,main] a tiré 3342
Thread[#21,Thread-1,5,main] a tiré 9445
Thread[#23,Thread-3,5,main] a tiré 5829
Max courant : 9445
...
Max final : 9445

On souhaite maintenant pouvoir afficher le thread qui a proposé le maximum en même temps que sa valeur. Modifier le code en conséquence.