Synchonisation des threads

variables partagées

Source de PerroquetsMatheux20.java
class PerroquetsMatheux20
{ 
  private int compteur;
  public static void main(String args[]) {
    new PerroquetsMatheux20();
  }    
  public PerroquetsMatheux20() {
    compteur = 0;
    Perroquet20 perroquetA = new Perroquet20("coco", 10);
    Perroquet20 perroquetB = new Perroquet20("mille sabord", 10);
    perroquetA.start();
    perroquetB.start();
    try { 
      perroquetA.join();
      perroquetB.join();
    }
    catch(InterruptedException e) {  }
    System.out.println("compteur = "+compteur);
  }
  
  class Perroquet20 extends Thread
  {
    private String cri = null;
    private int fois = 0;
    public Perroquet20(String s, int i)   { 
      cri = s;
      fois = i;
    }
    public void repeter() {
      String repete = cri + " " + compteur;
      System.out.println(repete);
      compteur++;
      try { 
        Thread.sleep((int)(Math.random()*1000)); 
      }
      catch(InterruptedException e) {  }
    }
    public void run(){
      for (int n=0; n<fois; n++)
        repeter();
    }
  }
}
EXECUTION
coco 0
mille sabord 1
mille sabord 2
coco 3
coco 4
mille sabord 5
mille sabord 6
coco 7
coco 8
mille sabord 9
coco 10
mille sabord 11
coco 12
mille sabord 13
coco 14
coco 15
mille sabord 16
coco 17
mille sabord 18
mille sabord 19
compteur = 20
  • les lignes de la classe :
    • du fait des règles de visibilité de Java, la variable compteur est visible/accessible à partir de la classe Perroquet20
      • donc des 2 objets threads perroquetA et perroquetB
      • par contre, les variables d'instance cri et fois de Perroquet20 existent en autant d'exemplaires que d'instances de Perroquet20.
    • les 2 threads accède donc à un espace partagé/commun de variables.
  • Contrairement au processus qui possède son propre espace de travail clairement séparé des autres processus, les threads sont exécutés au sein du même processus "java".

le problème de l'exclusion mutuelle

Source de PerroquetsMatheux21.java
class PerroquetsMatheux21
{ 
  private int compteur;
  public static void main(String args[]) {
    new PerroquetsMatheux21();
  }    
  public PerroquetsMatheux21() {
    compteur = 0;
    Perroquet21 perroquetA = new Perroquet21("coco", 10);
    Perroquet21 perroquetB = new Perroquet21("mille sabord", 10);
    perroquetA.setPriority(perroquetB.getPriority()%2);
    perroquetA.start();
    perroquetB.start();
    try { 
      perroquetA.join();
      perroquetB.join();
    }
    catch(InterruptedException e) {  }
    System.out.println("compteur = "+compteur);
  }
  
  class Perroquet21 extends Thread
  {
    private String cri = null;
    private int fois = 0;
    public Perroquet21(String s, int i)   { 
      cri = s;
      fois = i;
    }
    public void repeter() {
      int valeur = compteur + 1;
      String repete = cri + " " + valeur;
      System.out.println(repete);
      try { 
        Thread.sleep((int)(Math.random()*100)); 
      }
      catch(InterruptedException e) {  }
      compteur = valeur;    
      try { 
        Thread.sleep((int)(Math.random()*100)); 
      }
      catch(InterruptedException e) {  }
    }
    public void run(){
      for (int n=0; n<fois; n++)
        repeter();
    }
  }
}
EXECUTION
coco 1
mille sabord 1
coco 2
mille sabord 3
coco 3
mille sabord 4
coco 5
mille sabord 6
coco 7
mille sabord 8
coco 8
mille sabord 9
coco 9
mille sabord 10
coco 10
mille sabord 11
coco 11
coco 12
mille sabord 12
mille sabord 13
compteur = 13
  • les lignes de la classe :
    • les 2 threads perroquet travaillent alternativement :
      • un thread peut etre suspendu au milieu de l'exécution de sa méthode répeter pour que le controleur de thread laisse l'autre s'exécuter.
    • dans la version précédente des Perroquet20, l'instruction compteur++ est en fait
      • une lecture
      • une addition
      • une affectation
      • donc le thread pourrait etre suspendu, rarement mais possiblement, entre la lecture et l'écriture de compteur

bloc synchonisé

Source de PerroquetsMatheux22.java
class PerroquetsMatheux22
{ 
  private Compteur compteur;
  
  public static void main(String args[]) {
    new PerroquetsMatheux22();
  }    
  public PerroquetsMatheux22() {
    compteur = new Compteur();
    Perroquet22 perroquetA = new Perroquet22("coco", 10);
    Perroquet22 perroquetB = new Perroquet22("mille sabord", 10);
    perroquetA.setPriority(perroquetB.getPriority()%2);
    perroquetA.start();
    perroquetB.start();
    try { 
      perroquetA.join();
      perroquetB.join();
    }
    catch(InterruptedException e) {  }
    System.out.println("compteur = "+compteur.getValeur());
  }
  
  class Perroquet22 extends Thread
  {
    private String cri = null;
    private int fois = 0;
    public Perroquet22(String s, int i)   { 
      cri = s;
      fois = i;
    }
    public void repeter() {
      synchronized(compteur) {
        int valeur = compteur.getValeur() + 1;
        String repete = cri + " " + valeur;
        System.out.println(repete);
        try { 
          Thread.sleep((int)(Math.random()*100)); 
        }
        catch(InterruptedException e) {  }
        compteur.setValeur(valeur);    
      }
      try { 
        Thread.sleep((int)(Math.random()*100)); 
      }
      catch(InterruptedException e) {  }
    }
    public void run(){
      for (int n=0; n<fois; n++)
        repeter();
    }
  }
  
  class Compteur 
  { 
    private int valeur = 0;
    public int getValeur() { return valeur; }
    public void setValeur(int v) { valeur = v; }
  }
}
EXECUTION
coco 1
mille sabord 2
coco 3
mille sabord 4
coco 5
mille sabord 6
mille sabord 7
coco 8
mille sabord 9
coco 10
mille sabord 11
coco 12
mille sabord 13
coco 14
mille sabord 15
coco 16
mille sabord 17
coco 18
mille sabord 19
coco 20
compteur = 20
  • les lignes de la classe :
    • le mot-clé synchronized définit un bloc d'instruction qui ne peut s'exécuter qu'exclusivement même si plusieurs threads souhaitent l'exécuter :
      • lorsque le thread perroquetA exécute ce bloc synchronisé, et que le thread perroquetB souhaite commencer l'exécution de ce même bloc, alors le thread perroquetB doit attendre. Quand le thread perroquetA aura finit, le thread perroquetB pourra reprendre.
  • seul un thread à la fois peut exécuter un bloc synchronisé
    • on dit que le bloc en exclusion mutuelle ou encore que c'est une section critique
    • les autres threads, s'ils désirent exécuter cette section, doivent attendre que le thread en section critique la termine
    • si plusieurs threads attendent pour un même bloc synchronisé qui "se libère", le controleur de thread n'en autorisera qu'un à l'éxécuter.
  • L'appel à sleep() ne provoque pas de sortie de la section critique


méthode d'instance synchonisée

Source de PerroquetsMatheux23.java
class PerroquetsMatheux23
{ 
  private Compteur compteur;
  public static void main(String args[]) {
    new PerroquetsMatheux23();
  }    
  public PerroquetsMatheux23() {
    compteur = new Compteur();
    Perroquet23 perroquetA = new Perroquet23("coco", 10);
    Perroquet23 perroquetB = new Perroquet23("mille sabord", 10);
    perroquetA.setPriority(perroquetB.getPriority()%2);
    perroquetA.start();
    perroquetB.start();
    try { 
      perroquetA.join();
      perroquetB.join();
    }
    catch(InterruptedException e) {  }
    System.out.println("compteur = "+compteur);
  }
  
  class Perroquet23 extends Thread
  {
    private String cri = null;
    private int fois = 0;
    public Perroquet23(String s, int i)   { 
      cri = s;
      fois = i;
    }
    public void repeter() {
      int valeur = compteur.plus1();
      String repete = cri + " " + valeur;
      System.out.println(repete);
      try { 
        Thread.sleep((int)(Math.random()*100)); 
      }
      catch(InterruptedException e) {  }
    }
    public void run(){
      for (int n=0; n<fois; n++)
        repeter();
    }
  }
  
  class Compteur 
  { 
    private int valeur = 0;
    public synchronized int plus1() {
      return ++valeur;
    }
  }
}
EXECUTION
coco 1
mille sabord 2
coco 3
coco 5
mille sabord 4
coco 6
mille sabord 7
coco 8
mille sabord 9
mille sabord 10
coco 11
coco 12
coco 13
mille sabord 14
mille sabord 15
coco 16
mille sabord 17
coco 18
mille sabord 19
mille sabord 20
compteur = 20
  • les lignes de la classe :
    • le mot synchronised définit le bloc de la méthode en exclusion mutuelle
      • içi c'est l'objet compteur qui "monitorise"
  • la méthode synchonisée est aussi un mécanisme d'exclusion mutuelle sur une portion de code :
    • le moniteur qui supervise cette section critique est celui de l'objet surquel est appelée la méthode.
    • remarque :
      • synchronized méthode(paramètres) {
          bloc d'instructions
        }
      • est équivalent à :
      • méthode(paramètres) {
         synchronized(this) {
           bloc d'instructions
         }
        }
  • la synchronisation ralentit l'ensemble de l'exécution, donc il faut limiter le nombre de portion synchronisée et leur taille (en instructions)
    • il est possible de synchroniser sur une classe pour accéder en exclusion mutuelle sur les variables de classe
    • idem pour une méthode de classe synchronisée
  • Le mécanisme de "monitor" d'un objet s'applique à toutes les instances de Object :
    • c'est donc un mécanisme implémenté au coeur de JAVA
    • un seul thread peut être à la fois le propriétaire du moniteur d'un objet.

le problème de coopération des threads

Source de EcoleDesPerroquets14.java
public class EcoleDesPerroquets14 {  
  static String mot = null;
  public static void main(String[] args) {
    Perroquet14 perroquet1 = new Perroquet14("coco");
    perroquet1.start();
    Perroquet14 perroquet2 = new Perroquet14("jaco");
    perroquet2.start();
    String reponse = null;
    do {
      mot = reponse;
      System.out.println("nouveau mot pour perroquet ? (sinon non)");
      Thread.currentThread().yield();
      reponse = Clavier.lireString();
    }
    while (! reponse.equals("non"));
    System.exit(1);
  }
}

class Perroquet14 extends Thread {
  private String nom;
  public Perroquet14(String n)   { 
    super(n);
    nom = n;
  }
  public void repeter() {
    System.out.println(nom + " "+ EcoleDesPerroquets14.mot);
  }
  public void run() {
    while (true) {
      while (EcoleDesPerroquets14.mot == null)
        try {
          Thread.sleep(2000);     
        } catch (Exception e) { }
      for (int n=0; n<3; n++) 
        repeter();
      try { 
        Thread.sleep((int)(Math.random()*3000)); 
      }
      catch(InterruptedException e) {  }
    }
  }
}
EXECUTION
nouveau mot pour perroquet ? (sinon non)
bla
nouveau mot pour perroquet ? (sinon non)
coco bla
coco bla
coco bla
jaco bla
jaco bla
jaco bla
blu
nouveau mot pour perroquet ? (sinon non)
coco blu
coco blu
coco blu
blo
coco blu
coco blu
coco blu
coco blu
coco blu
coco blu
nouveau mot pour perroquet ? (sinon non)
jaco blo
jaco blo
jaco blo
jaco blo
jaco blo
jaco blo
coco blo
coco blo
coco blo
.....
  • les lignes de la classe :
    • Une variable static "mot" est partagée entre les 2 threads perroquets qui doivent l'apprendre puis le répéter et le thread main qui en saisit un nouveau.
    • l'ensemble n'est pas du tout synchronisée :
      • les perroquets ne savent pas si le mot est un nouveau à apprendre ou si c'est l'ancien.
      • Au début, ils doivent attendre avec une boucle pour le premier mot
      • et si le user/professeur fournit trop rapidement des nouveaux mots, les 2 perroquets peuvent "en rater" !
  • Le but :
    • Il faudrait avoir un mécanisme d'attente entre le professeur (user) et les élèves perroquets :
      • les élèves attendent un nouveau mot à apprendre
      • le professeur, quand il enseigne un nouveau mot, devrait attendre que les élèves aient le temps de l'apprendre et le répeter, avant d'en enseigner un nouveau.

wait et notifyAll

Source de EcoleDesPerroquets15.java
public class EcoleDesPerroquets15 {  
  public static void main(String[] args) {
    AuTableau autableau = new AuTableau();
    Perroquet15 perroquet1 = new Perroquet15("coco", autableau);
    perroquet1.start();
    Perroquet15 perroquet2 = new Perroquet15("jaco", autableau);
    perroquet2.start();
    String reponse = "bonjour";
    do {
      autableau.enseigner(reponse);
      System.out.println("nouveau mot pour perroquet ? (sinon non)");
      Thread.currentThread().yield();
      reponse = Clavier.lireString();
    }
    while (! reponse.equals("non"));
    System.exit(1);
  }
}
class Perroquet15 extends Thread {
  private String cri;
  private String nom;
  private AuTableau autableau;
  public Perroquet15(String n, AuTableau a)   { 
    super(n);
    nom = n;
    autableau = a ;
    cri = "";
  }
  public void repeter() {
    System.out.println(nom + " "+ cri);
  }
  public void run() {
    while (true) {
      cri = autableau.apprendre();
      for (int n=0; n<3; n++) 
        repeter();
    }
  }
}

class AuTableau {
  private String motAapprendre = null;
  synchronized String apprendre() {
    try {
      wait();     
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
    return motAapprendre;
  }
  synchronized void enseigner (String mot) {
    motAapprendre = mot ;
    notifyAll();    
  }
}
EXECUTION
nouveau mot pour perroquet ? (sinon non)
coco bonjour
coco bonjour
coco bonjour
miam
nouveau mot pour perroquet ? (sinon non)
coco miam
coco miam
coco miam
jaco miam
jaco miam
jaco miam
encore
coco encore
coco encore
coco encore
jaco encore
jaco encore
jaco encore
nouveau mot pour perroquet ? (sinon non)
non

 

notify

Source de EcoleDesPerroquets16.java
public class EcoleDesPerroquets16 {  
  public static void main(String[] args) {
    AuTableau autableau = new AuTableau();
    Perroquet16 perroquet1 = new Perroquet16("coco", autableau);
    perroquet1.start();
    Perroquet16 perroquet2 = new Perroquet16("jaco", autableau);
    perroquet2.start();
    String reponse = "bonjour";
    do {
      autableau.enseigner(reponse);
      System.out.println("nouveau mot pour perroquet ? (sinon non)");
      Thread.currentThread().yield();
      reponse = Clavier.lireString();
    }
    while (! reponse.equals("non"));
    System.exit(1);
  }
}
class Perroquet16 extends Thread {
  private String cri;
  private String nom;
  private AuTableau autableau;
  public Perroquet16(String n, AuTableau a)   { 
    super(n);
    nom = n;
    autableau = a ;
    cri = "";
  }
  public void repeter() {
    System.out.println(nom + " "+ cri);
  }
  public void run() {
    while (true) {
      cri = autableau.apprendre();
      for (int n=0; n<3; n++) 
        repeter();
    }
  }
}
class AuTableau {
  private String motAapprendre = null;
  synchronized String apprendre() {
    try {
      wait();     
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
    return motAapprendre;
  }
  synchronized void enseigner (String mot) {
    motAapprendre = mot ;
    notify();    
  }
}
EXECUTION
coco bonjour
coco bonjour
miam
coco miam
coco miam
coco miam
nouveau mot pour perroquet ? (sinon non)
encore
jaco encore
jaco encore
jaco encore
nouveau mot pour perroquet ? (sinon non)
bizarre
coco bizarre
coco bizarre
coco bizarre
nouveau mot pour perroquet ? (sinon non)
vraiment
jaco vraiment
jaco vraiment
jaco vraiment
nouveau mot pour perroquet ? (sinon non)
non
  • les lignes de la classe :
    • un seul perroquet élève apprend à la fois
    • un seul therad est libéré de l'attente par notify
  • notify() ne réveille qu'un thread à la fois
    • il n'y a pas de spécification sur le thread choisi ! c'est le controlleur de thread qui choisit.

demi-synchronisation

Source de EcoleDesPerroquets17.java
public class EcoleDesPerroquets17 {  
  public static void main(String[] args) {
    AuTableau autableau = new AuTableau();
    Perroquet17 perroquet1 = new Perroquet17("coco", autableau);
    perroquet1.start();
    Perroquet17 perroquet2 = new Perroquet17("jaco", autableau);
    perroquet2.start();
    String reponse = "bonjour";
    do {
      autableau.enseigner(reponse);
      System.out.println("nouveau mot pour perroquet ? (sinon non)");
      Thread.currentThread().yield();
      reponse = Clavier.lireString();
    }
    while (! reponse.equals("non"));
    System.exit(1);
  }
}
class Perroquet17 extends Thread {
  private String cri;
  private String nom;
  private AuTableau autableau;
  public Perroquet17(String n, AuTableau a)   { 
    super(n);
    nom = n;
    autableau = a ;
    cri = "";
  }
  public void repeter() {
    System.out.println(nom + " "+ cri);
  }
  public void run() {
    while (true) {
      cri = autableau.apprendre();
      for (int n=0; n<3; n++) {
        repeter();
        try { 
          Thread.sleep(2000); 
        }
        catch(InterruptedException e) { }
      }
    }
  }
}
class AuTableau {
  private String motAapprendre = null;
  synchronized String apprendre() {
    try {
      wait();     
    } catch (Exception e) {
      e.printStackTrace();
      System.exit(1);
    }
    return motAapprendre;
  }
  synchronized void enseigner (String mot) {
    motAapprendre = mot ;
    notifyAll();    
  }
}
EXECUTION
nouveau mot pour perroquet ? (sinon non)
coco bonjour
coco bonjour
coco bonjour
Miam
nouveau mot pour perroquet ? (sinon non)
jaco Miam
jaco Miam
jaco Miam
1 2 3 4
nouveau mot pour perroquet ? (sinon non)
coco 1
nouveau mot pour perroquet ? (sinon non)
nouveau mot pour perroquet ? (sinon non)
jaco 3
nouveau mot pour perroquet ? (sinon non)
coco 1
jaco 3
coco 1
jaco 3
non
  • les lignes de la classe :
    • les perroquets répètent puis se mettent en attente d'un nouveau mot à apprendre
    • dès qu'un nouveau mot est au tableau, les perroquets en attente sont notifiés.
    • Mais les perroquets qui ne sont pas en attente perdront certains mots si le professeur va trop vite.
    • C'est une demi-synchronisation :
      • le professeur n'attend pas que les perroquets aient eu le temps de lire et répéter.
         
          
  • wait(long délai) est une attente avec un délai d'expiration

Problème du Producteur et du Consommateur

Source de EcoleDuPerroquet18.java
public class EcoleDuPerroquet18 {  
  public static void main(String[] args) {
    AuTableau autableau = new AuTableau();
    Perroquet18 perroquet = new Perroquet18("coco", autableau);
    perroquet.start();
    Maitre maitre = new Maitre(autableau);
    maitre.start();
  }
}
class Maitre  extends Thread {
  private AuTableau autableau;
  private String reponse = null;
  public Maitre (AuTableau a)   { 
     autableau = a ;
  }
  public void run() {
    for (int n=0; n<4; n++) {
      System.out.println("nouveau mot a enseigner au  perroquet ?");
      Thread.currentThread().yield();
      reponse = Clavier.lireString();
      autableau.enseigner(reponse);
    }
  }
}
    
class Perroquet18 extends Thread {
  private String cri;
  private String nom;
  private AuTableau autableau;
  public Perroquet18(String n, AuTableau a)   { 
    super(n);
    nom = n;
    autableau = a ;
    cri = "";
  }
  public void repeter() {
    System.out.println(nom + " "+ cri);
  }
  public void run() {
    while (true) {
      cri = autableau.apprendre();
      for (int n=0; n<3; n++) {
        repeter();
        try { 
          Thread.sleep(2000); 
        }
        catch(InterruptedException e) { }
      }
    }
  }
}
class AuTableau {
  private String motAuTableau = null;
  synchronized String apprendre() {
    String motAapprendre;
    while (motAuTableau  == null)
      try {
        wait();     
      } catch (Exception e) {}
    motAapprendre = motAuTableau  ;
    motAuTableau  = null;
    notify();   
    return motAapprendre;
  }
  synchronized void enseigner (String motNouveau) {
    while (motAuTableau  != null)
      try {
        wait();     
      } catch (Exception e) {}
    motAuTableau = motNouveau;
    notify();   
  }
}
EXECUTION
nouveau mot a enseigner au  perroquet ?
mille
coco mille
nouveau mot a enseigner au  perroquet ?
coco mille
coco mille
sabords
coco sabords
nouveau mot a enseigner au  perroquet ?
coco sabords
coco sabords
non
coco non
nouveau mot a enseigner au  perroquet ?
coco non
coco non
  • les lignes de la classe :
    • le problème est simplifié :
      • un seul professeur
      • un seul perroquet
      • et chacun attend l'autre :
        • le perroquet attend pour un nouveau mot à apprendre
        • le professeur attend que le perroquet ait lu puis répété le mot
  • Ce pattern classique est appelé Producteur-Consommateur :
    • il faut synchroniser le fonctionnement de chacun afin que :
      • le consommateur attende quand il n'y a rien à consommer
      • le producteur attende quand le consommateur n'est pas pret à consommer.

Interblocage

Source de Interblocage.java
public class Interblocage {
  public static void main(String[] args) {
    final int[] tab1 = { 1, 2, 3, 4 };
    final int[] tab2 = { 0, 1, 0, 1 };
    /* tab1 et tab2 sont susceptibles d'etre modifiés 
       par d'autres threads */
    final int[] tabAdd = new int[4];
    final int[] tabSub = new int[4];

    // réalise l'addition tabAdd = tab1 + tab2
    Thread tacheAdd = new Thread() {
      public void run() {
        synchronized(tab1) {
          System.out.println("Thread tacheAdd lock tab1");
          travailHarassant();
          synchronized(tab2) {
            System.out.println("Thread tacheAdd lock tab2");
            for (int i=0; i<4 ; i++)
              tabAdd[i] = tab1[i] + tab2[i];
          }
        }
      }
    };

    // réalise la soustraction tabAdd = tab1 - tab2
    Thread tacheSub = new Thread() {
      public void run() {
        synchronized(tab2) {
          System.out.println("Thread tacheSub lock tab2");
          travailHarassant();
          synchronized(tab1) {
            System.out.println("Thread tacheSub lock tab1");
            for (int i=0; i<4 ; i++)
              tabAdd[i] = tab1[i] - tab2[i];
          }
        }
      }
    };
    tacheAdd.start(); 
    tacheSub.start();
  }
  static void travailHarassant() {
    try { 
      Thread.sleep((int)(Math.random()*50+25));
    }
    catch (InterruptedException e) {}
  }
}
EXECUTION
Thread tacheAdd lock tab1
Thread tacheSub lock tab2

<ctrl-C>
  • les lignes de la classe :
    • Supposons que tab1 et tab2 sont susceptibles d'être modifiés par d'autres threads qui effecturaient ces modifications en acquérant leur verrou
  • Pour éviter une modification au cours de leur calcul, les 2 threads cherchent à obtenir les verrous de tab1 et tab2
  • Le problème d'interblocage est que :
    • la tacheAdd a obtenu le lock de tab1 et attend pour obtenir celui de tab2, puis ils les libérera tous les 2.
    • la tacheSub a obtenu le lock de tab2 et attend pour obtenir celui de tab1, puis ils les libérera tous les 2.
    • Ils sont donc bloqués
  • c'est un Deadlock
    • un interblocage de taches parallèles
      • qui ont acquis le verrou de certaines ressources et cherchent à en obtenir d'autres
      • le graphe des verrouillages est insastifaisable
      • comme les taches ne peuvent revenir en arrière (cad libérer des ressources), la situation est définitivement bloquée

exercices