Synchronisation de Threads

    3.4.1 Partage des ressources sans synchronisation

Dans une relation producteur - consommateur, un thread producteur appelle une méthode de production qui dépose un séquence de nombres (utilisons 1, 2, 3, ...) dans une zone de mémoire partagée. Le thread consommateur lit ces données.

Dans notre exemple, le tampon (buffer) sera présenté par une classe "Entier" qui a la capacité de stocker un seul entier (intPartage).  La classe fournit deux méthodes pour lire (getIntPartage) et modifier (setIntPartage) le contenu de tampon.

Les deux classes producteur (ProduitEntier) et consomateur (ConsommeEntier) sont  dérivées directement de la classe Thread. Le producteur produit 10 entiers et les met dans le tampon. Le consommateur les lit dans l'ordre et termine lorsqu'il lit le dernier nombre - 10.

public class Entier {
    private int intPartage = -1;
   public void setIntPartage( int val ){
      System.out.println( Thread.currentThread().getName() +
                 " met intPartage а " + val );
      intPartage = val;
   }
    public int getIntPartage(){
       System.out.println( Thread.currentThread().getName() +
                 " recupere intPartage " + intPartage );
       return intPartage;
   }
}

public class ProduitEntier extends Thread {
   private Entier pGarde;
   public ProduitEntier( Entier h ){
       super( "ProduitEntier" );
       pGarde = h;
   }
   public void run(){
      for ( int compteur = 1; compteur <= 10; compteur++ ) {
          // dormir pour une duree aleatoire.
          try {Thread.sleep( (int) ( Math.random() * 30 ) );}
          catch( InterruptedException e ) {
              System.err.println( e.toString() );
          }
          pGarde.setIntPartage( compteur );
      }
      System.out.println( getName() + " a termine"  );
   }
}

public class ConsommeEntier extends Thread {
   private Entier cGarde;
   public ConsommeEntier( Entier h ){
      super( "ConsommeEntier" );
      cGarde = h;
   }
   public void run(){
      int val, somme = 0;
      do {
         // dormir pour une duree aleatoire.
         try {Thread.sleep( (int) ( Math.random() * 1000 ) );}
         catch( InterruptedException e ) {
              System.err.println( e.toString() );
         }
         val = cGarde.getIntPartage();
         somme += val;
       } while ( val != 10 );
       System.out.println(
           getName() + " a lu des valeurs dont le total vaut: " + somme);
   }
}

public class CellulePartagee {
   public static void main( String args[] ){
      Entier h =new Entier();
      ProduitEntier p = new ProduitEntier( h );
      ConsommeEntier c = new ConsommeEntier( h );
      p.start();
      c.start();
   }
}

ConsommeEntier recupere intPartage -1
ConsommeEntier recupere intPartage -1
ProduitEntier met intPartage а 1
ConsommeEntier recupere intPartage 1
ProduitEntier met intPartage а 2
ConsommeEntier recupere intPartage 2
ConsommeEntier recupere intPartage 2
ConsommeEntier recupere intPartage 2
ProduitEntier met intPartage а 3
ConsommeEntier recupere intPartage 3
ConsommeEntier recupere intPartage 3
ConsommeEntier recupere intPartage 3
ConsommeEntier recupere intPartage 3
ConsommeEntier recupere intPartage 3
ProduitEntier met intPartage а 4
ConsommeEntier recupere intPartage 4
ConsommeEntier recupere intPartage 4
ConsommeEntier recupere intPartage 4
ConsommeEntier recupere intPartage 4
ConsommeEntier recupere intPartage 4
ProduitEntier met intPartage а 5
ConsommeEntier recupere intPartage 5
ConsommeEntier recupere intPartage 5
ConsommeEntier recupere intPartage 5
ProduitEntier met intPartage а 6
ConsommeEntier recupere intPartage 6
ConsommeEntier recupere intPartage 6
ConsommeEntier recupere intPartage 6
ConsommeEntier recupere intPartage 6
ConsommeEntier recupere intPartage 6
ConsommeEntier recupere intPartage 6
ProduitEntier met intPartage а 7
ConsommeEntier recupere intPartage 7
ConsommeEntier recupere intPartage 7
ConsommeEntier recupere intPartage 7
ConsommeEntier recupere intPartage 7
ProduitEntier met intPartage а 8
ConsommeEntier recupere intPartage 8
ConsommeEntier recupere intPartage 8
ProduitEntier met intPartage а 9
ConsommeEntier recupere intPartage 9
ConsommeEntier recupere intPartage 9
ConsommeEntier recupere intPartage 9
ConsommeEntier recupere intPartage 9
ConsommeEntier recupere intPartage 9
ConsommeEntier recupere intPartage 9
ProduitEntier met intPartage а 10
ProduitEntier a termine
ConsommeEntier recupere intPartage 10
ConsommeEntier a lu des valeurs dont le total vaut: 199
 

  

Risques possibles :

  1. Le producteur et le consommateur peuvent tous deux essayer de mettre à jour le contenu de tampon en même temps. Cela peut entraîner une perte de données ou des incohérences.
  2. Le producteur pourrait être plus lent que le consommateur - risque de lire la même valeur plusieurs fois.
  3. Le consommateur pourrait être plus lent que le producteur - risque de perdre des valeurs produites.

3.4.2 Synchronisation

Java utilise ce que l'on appelle des "moniteurs" pour assurer la synchronisation. Le moniteur représente un objet qui permet a un seul thread à la fois d'exécuter une méthode ou un bloc de code. Ceci est accomplit en verrouillant l'objet lors de l'invocation de la méthode ou du bloc de code.

On utilise le mot clé synchronized pour marqué qu'une méthode

public synchronized int methode1{
      ...
}

ou bien un bloc de code

synchronized(objet)
    while(...) {
           ...
    }

devra verrouillé l'objet quand un thread entre dans la méthode ou bien dans le bloc. On dit aussi que l'ont obtient le verrouillage. S'il y a plusieurs méthode  synchronized , un seul de ces méthode est active à la fois sur un objet, tous les autres objets qui tentent d'invoquer des méthodes synchronized  doivent attendre. Quand la méthode synchronized  termine son exécution, le verrou sur l'objet est libéré et le moniteur laisse le thread prêt de la plus haute priorité tenter d'invoquer  l'exécution d'une méthode synchronized.

Si un thread qui s'exécute dans une méthode synchronized  détermine, qu'il ne peut poursuivre, alors il appel wait() va dans l'état "blocked" et libère le verrouillage de l'objet. Pour sortir de cette état elle devra recevoir notyfy() d'un autre thread qui sorte d'une méthode synchronized.

notifyAll();

    3.4.3 Producteur - consommateur avec synchronisation:

public class Entier {
    private int intPartage = -1;
    private boolean inscriptible= true;
   public synchronized void setIntPartage( int val ){
      while(!inscriptible) {
          try{     wait();   }
          catch(InterruptedException e){
              System.err.println(e);
          }
      }
      System.out.println( Thread.currentThread().getName() +
                 " met intPartage а " + val );
      intPartage = val;
      inscriptible = false;
      notify();
   }
    public synchronized int getIntPartage(){
      while(inscriptible) {
          try{     wait();   }
          catch(InterruptedException e){
              System.err.println(e);
          }
      }
       System.out.println( Thread.currentThread().getName() +
                 " recupere intPartage " + intPartage );
       inscriptible = true;
       notify();
       return intPartage;
   }
}
ProduitEntier met intPartage а 1
ConsommeEntier recupere intPartage 1
ProduitEntier met intPartage а 2
ConsommeEntier recupere intPartage 2
ProduitEntier met intPartage а 3
ConsommeEntier recupere intPartage 3
ProduitEntier met intPartage а 4
ConsommeEntier recupere intPartage 4
ProduitEntier met intPartage а 5
ConsommeEntier recupere intPartage 5
ProduitEntier met intPartage а 6
ConsommeEntier recupere intPartage 6
ProduitEntier met intPartage а 7
ConsommeEntier recupere intPartage 7
ProduitEntier met intPartage а 8
ConsommeEntier recupere intPartage 8
ProduitEntier met intPartage а 9
ConsommeEntier recupere intPartage 9
ProduitEntier met intPartage а 10
ProduitEntier a termine
ConsommeEntier recupere intPartage 10
ConsommeEntier a lu des valeurs dont le total vaut: 55
 

 

   

     3.4.4 Plusieurs producteurs et consommateurs

Un consommateur doit terminer quand tous les producteurs ont terminé et il n'y a aucune valeur produit - compteur des producteurs actifs. Exception pour terminer. "Timed waiting" pour ne pas rater la terminaision du dernier producteur.

public class Entier {
    private int intPartage = -1;
    private int nomP =0;
    private boolean inscriptible= true;
    public synchronized void incP(){        nomP++;    }
    public synchronized void decP(){        nomP--;    }
   public synchronized void setIntPartage( int val ){
      while(!inscriptible) {
          System.out.println("\t"+Thread.currentThread().getName()+" waiting");
          try{     wait();   }
          catch(InterruptedException e){
              System.err.println(e);
          }
      }
      System.out.println( Thread.currentThread().getName() +
                 " met intPartage a " + val );
      intPartage = val;
      inscriptible = false;
      notifyAll();
   }
    public synchronized int getIntPartage() throws NoP{
      while(inscriptible) {
          if(nomP==0)  throw new NoP()  ;
          System.out.println("\t"+Thread.currentThread().getName()+" waiting");
          try{     wait(1000);  }
          catch(InterruptedException e){
              System.err.println(e);
          }
      }
       System.out.println( Thread.currentThread().getName() +
                 " recupere intPartage " + intPartage );
       inscriptible = true;
       notifyAll();
       return intPartage;
   }
}
-----------------------------------------------------------------------------
public class ProduitEntier extends Thread {
   private Entier pGarde;
   public ProduitEntier( Entier h ){
       super( "ProduitEntier " +(int)(Math.random()*1000));
       pGarde = h;
   }
   public void run(){
       pGarde.incP();
       System.out.println( getName() + " starting"  );
      for ( int compteur = 1; compteur <= 4; compteur++ ) {
          // dormir pour une duree aleatoire.
          try {Thread.sleep( (int) ( Math.random() * 3000 ) );}
          catch( InterruptedException e ) { System.err.println( e.toString() );   }
          pGarde.setIntPartage( compteur );
      }
      pGarde.decP();
      System.out.println( getName() + " a termine"  );   
   }
}
-----------------------------------------------------------------------------
public class ConsommeEntier extends Thread {
    private Entier cGarde;
    public ConsommeEntier( Entier h ){
        super( "ConsommeEntier "+(int)(Math.random()*1000) );
        cGarde = h;
    }
    public void run(){
        System.out.println( getName() + " starting"  );
        int val;
        do {
            try {Thread.sleep( (int) ( Math.random() * 1000 ) );}
            catch( InterruptedException e ) {
                System.err.println( e.toString() );
            }
            try{
                val = cGarde.getIntPartage();
            }catch (NoP exc){
                break;
            }
            System.out.println("\t\t"+this.getName()+" a lu la valeur "+val);
        } while ( true );
        System.out.println( getName() + " a fini");
    }
}
----------------------------------------------------------------------------
public class NoP extends Exception{
    NoP(){
        System.out.println("Exception NoP jete");
    }
  
}
---------------------------------------------------------------------------
public class CellulePartagee {
    public static void main( String args[] ){
        Entier h =new Entier();
        for(int i=0;i<3;i++){
            (new ProduitEntier( h )).start();
        }
        System.out.println("All ProduitEntier started by: "+ Thread.currentThread().getName());
        for(int i=0;i<2;i++){
            (new ConsommeEntier(h)).start();
        }
        System.out.println("Thread "+Thread.currentThread().getName()+" finished");
    }
}
ProduitEntier 828 starting
ProduitEntier 836 starting
ProduitEntier 707 starting
ConsommeEntier 191 starting
ConsommeEntier 114 starting
    ConsommeEntier 191 waiting
ProduitEntier 828 met intPartage a 1
ConsommeEntier 191 recupere intPartage 1
        ConsommeEntier 191 a lu la valeur 1
    ConsommeEntier 114 waiting
    ConsommeEntier 191 waiting
ProduitEntier 707 met intPartage a 1
ConsommeEntier 191 recupere intPartage 1
        ConsommeEntier 191 a lu la valeur 1
    ConsommeEntier 114 waiting
    ConsommeEntier 191 waiting
ProduitEntier 828 met intPartage a 2
ConsommeEntier 191 recupere intPartage 2
        ConsommeEntier 191 a lu la valeur 2
    ConsommeEntier 114 waiting
ProduitEntier 836 met intPartage a 1
ConsommeEntier 114 recupere intPartage 1
        ConsommeEntier 114 a lu la valeur 1
    ConsommeEntier 191 waiting
ProduitEntier 828 met intPartage a 3
ConsommeEntier 191 recupere intPartage 3
        ConsommeEntier 191 a lu la valeur 3
    ConsommeEntier 114 waiting
    ConsommeEntier 191 waiting
ProduitEntier 836 met intPartage a 2
ConsommeEntier 191 recupere intPartage 2
        ConsommeEntier 191 a lu la valeur 2
    ConsommeEntier 114 waiting
ProduitEntier 707 met intPartage a 2
ConsommeEntier 114 recupere intPartage 2
        ConsommeEntier 114 a lu la valeur 2
ProduitEntier 836 met intPartage a 3
    ProduitEntier 836 waiting
ConsommeEntier 114 recupere intPartage 3
ProduitEntier 836 met intPartage a 4
        ConsommeEntier 114 a lu la valeur 3
ProduitEntier 836 a termine
ConsommeEntier 191 recupere intPartage 4
        ConsommeEntier 191 a lu la valeur 4
ProduitEntier 828 met intPartage a 4
ProduitEntier 828 a termine
ConsommeEntier 191 recupere intPartage 4
        ConsommeEntier 191 a lu la valeur 4
ProduitEntier 707 met intPartage a 3
ConsommeEntier 114 recupere intPartage 3
        ConsommeEntier 114 a lu la valeur 3
    ConsommeEntier 191 waiting
    ConsommeEntier 114 waiting
    ConsommeEntier 191 waiting
ProduitEntier 707 met intPartage a 4
ProduitEntier 707 a termine
ConsommeEntier 191 recupere intPartage 4
        ConsommeEntier 191 a lu la valeur 4
Exception NoP jete
ConsommeEntier 114 a fini
Exception NoP jete
ConsommeEntier 191 a fini

    3.4.5 Exercice

Modifiez le 3.4.4 afin que le tampon ait la capacité de stocker plusieurs entiers (par exemple 3).