IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 14 - Les Threads multiples

pages : 1 2 3 4 5 6 7 8 9 

Considérons une variation sur les compteurs déjà utilisées plus haut dans ce chapitre. Dans l'exemple suivant, chaque thread contient deux compteurs incrémentés et affichés dans run(). En plus, un autre thread de la classe Watcher regarde les compteurs pour voir si ils restent toujours les même. Ceci apparaît comme une activité inutile puisqu'en regardant le code il apparaît évident que que les compteurs resteront toujours les même. Mais c'est là que la surprise arrive. Voici la première version du programme:


//: c14:Sharing1.java
// Les problèmes avec le partage
// de ressource et les threads
// <applet code=Sharing1 width=350 height=500>
// <param name=size value="12">
// <param name=watchers value="15">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Sharing1 extends JApplet {
  private static int accessCount = 0;
  private static JTextField aCount =
    new JTextField("0", 7);
  public static void incrementAccess() {
    accessCount++;
    aCount.setText(Integer.toString(accessCount));
  }
  private JButton
    start = new JButton("Start"),
    watcher = new JButton("Watch");
  private boolean isApplet = true;
  private int numCounters = 12;
  private int numWatchers = 15;
  private TwoCounter[] s;
  class TwoCounter extends Thread {
    private boolean started = color="#0000ff">false;
    private JTextField
      t1 = new JTextField(5),
      t2 = new JTextField(5);
    private JLabel l =
      new JLabel("count1 == count2");
    private int count1 = 0, count2 = 0;
    // Ajoute les composants visuels comme un panel
    public TwoCounter() {
      JPanel p = new JPanel();
      p.add(t1);
      p.add(t2);
      p.add(l);
      getContentPane().add(p);
    }
    public void start() {
      if(!started) {
        started = true;
        super.start();
      }
    }
    public void run() {
      while (true) {
        t1.setText(Integer.toString(count1++));
        t2.setText(Integer.toString(count2++));
        try {
          sleep(500);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
    public void synchTest() {
      Sharing1.incrementAccess();
      if(count1 != count2)
        l.setText("Unsynched");
    }
  }
  class Watcher extends Thread {
    public Watcher() { start(); }
    public void run() {
      while(true) {
        for(int i = 0; i < s.length; i++)
          s[i].synchTest();
        try {
          sleep(500);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      for(int i = 0; i < s.length; i++)
        s[i].start();
    }
  }
  class WatcherL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      for(int i = 0; i < numWatchers; i++)
        new Watcher();
    }
  }
  public void init() {
    if(isApplet) {
      String counters = getParameter("size");
      if(counters != null)
        numCounters = Integer.parseInt(counters);
      String watchers = getParameter("watchers");
      if(watchers != null)
        numWatchers = Integer.parseInt(watchers);
    }
    s = new TwoCounter[numCounters];
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < s.length; i++)
      s[i] = new TwoCounter();
    JPanel p = new JPanel();
    start.addActionListener(new StartL());
    p.add(start);
    watcher.addActionListener(new WatcherL());
    p.add(watcher);
    p.add(new JLabel("Access Count"));
    p.add(aCount);
    cp.add(p);
  }
  public static void main(String[] args) {
    Sharing1 applet = new Sharing1();
    // Ce n'est pas une applet, donc désactive le flag et
    // produit la valeur du paramètre depuis les arguments:
    applet.isApplet = false;
    applet.numCounters =
      (args.length == 0 ? 12 :
        Integer.parseInt(args[0]));
    applet.numWatchers =
      (args.length < 2 ? 15 :
        Integer.parseInt(args[1]));
    Console.run(applet, 350,
      applet.numCounters * 50);
  }
} ///:~

Comme avant, chaque compteur contient ses propres composants graphiques: deux champs textes et un label qui au départ indique que les comptes sont equivalents. Ces composants sont ajoutés au content pane de l'objet de la classe externe dans le constructeur de TwoCounter.

Comme un thread TwoCounter est démarré par l'utilisateur en pressant un bouton, il est possible que start() puisse être appelé plusieurs fois. Il est illégal d'appeler Thread.start() plusieurs fois pour une même thread (une exception est lancée). Vous pouvez voir la machinerie éviter ceci avec le flag started et la méthode redéfinie start().

Dans run(), count1 and count2 sont incrémentés et affichés de manière à ce qu'ils devraient rester identiques. Ensuite sleep() est appelé; sans cet appel le programme feinte [balks] parce qu'il devient difficile pour le CPU de passer d'une tache à l'autre.

La méthode synchTest() effectue l'activité apparemment inutile de vérifier si count1 est équivalent à count2; si ils ne sont pas équivalent elle positionne l'étiquette à « Unsynched » pour indiquer ceci. Mais d'abord, elle appelle une méthode statique de la classe Sharing1 qui incrémente et affiche un compteur d'accès pour montrer combien de fois cette vérification c'est déroulée avec succès. (La raison de ceci apparaîtra dans les prochaines variations de cet exemple.)

La classe Watcher est un thread dont le travail est d'appeler synchTest() pour tous les objets TwoCounter actifs. Elle le fait en parcourant le tableau maintenu dans l'objet Sharing1. Vous pouvez voir le Watcher comme regardant constamment par dessus l'épaule des objets TwoCounter.

Sharing1 contient un tableau d'objets TwoCounter qu'il initialise dans init() et démarre comme thread quand vous pressez le bouton « start ». Plus tard, quand vous pressez le bouton « Watch », un ou plusieurs watchers sont créés et freed upon the unsuspecting TwoCounter threads. [NDT: mon dico est gros mais j'y comprend rien...]

Notez que pour faire fonctionner ceci en tant qu'applet dans un browser, votre tag d'applet devra contenir ces lignes:


<param name=size value="20">
<param name=watchers value="1">

Vous pouvez expérimenter le changement de la largeur (width), la hauteur (height), et des paramètres pour l'adapter à vos goûts. En changeant size et watchers vous changerez le comportement du programme. Ce programme est prévu pour fonctionner comme une application autonome en passant les arguments sur la ligne de commande (ou en utilisant les valeurs par défaut).

La surprise arrive ici. Dans TwoCounter.run(), la boucle infinie se répète en exécutant seulement les deux lignes suivantes:


t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));

(elle dort aussi mais ce n'est pas important ici). En exécutant le programme, vous découvrirez que count1 et count2 seront observés (par les Watchers) comme étant inégaux par moments. A ce moment, la suspension a eu lieu entre l'exécution des deux lignes précédentes, et le thread Watcher s'est exécuté et a effectuée la comparaison juste à ce moment, trouvant ainsi les deux compteurs différents.

Cet exemple montre un problème fondamental de l'utilisation des threads. Vous ne savez jamais quand un threads sera exécuté. Imaginez-vous assis à une table avec une fourchette, prêt à piquer la dernière bouchée de nourriture dans votre assiette et comme votre fourchette est prête à la prendre, la bouchée disparaît soudainement (parce que votre thread a été suspendu et qu'un autre thread est venu voler votre nourriture). C'est ce problème qu'il faut traiter.

Quelques fois vous ne faites pas attention si une ressource est accéder en même temps que vous essayez de l'utiliser (la bouchée est sur une autre assiette). Mais pour que le multithreading fonctionne, vous avez besoin d'un moyen d'empêcher que deux threads accèdent à la même resource, particulièrement durant les périodes critiques.

Prévoir ce type de collisions est simplement un problème de placer un verrou sur une ressource quand un thread y accède. Le premier thread qui accède à la ressource la verrouille, ensuite les autres threads ne peuvent plus accéder à cette ressource jusqu'à ce qu'elle soit déverrouillée, à chaque fois un autre thread la verrouille et l'utilise, etc. Si le siège avant d'une voiture est la ressource limitée, les enfants qui crient « à moi! » revendiquent le verrou.

Comment Java partage les ressources

Java possède un support intégré pour prévoir les collisions sur un type de ressources: la memoire est un objet. Alors que vous rendez typiquement les éléments de données d'une classe private et accéder à cette mémoire seulement à travers des méthodes, vous pouvez prévoir les collisions en rendant une méthode particulière synchronized. Un seul thread à la fois peut appeler une méthode synchronized pour un objet particulier (bien que ce thread puisse appeler plus d'une des méthodes synchronized sur cet objet). Voici des méthodes synchronized simples:


synchronized void f() { /* ... */ }
synchronized void g(){ /* ... */ }

Chaque objet contient un seul lock (également appelé monitor) qui fait automatiquement partie de l'objet (vous n'avez pas à écrire de code spécial). Quand vous appeler une méthode synchronized, cet objet est verrouillé et aucune autre méthode synchronized de cet objet ne peut être appelé jusqu'à ce que la première se termine et libère le verrou. Dans l'exemple précédent, si f() est appelé pour un objet, g() ne peut pas être appelé pour le même objet jusqu'à ce que f() soit terminé et libère le verrou. Ainsi, il y a un seul verrou partagé par toutes les méthodes synchronized d'un objet particulier et ce verrou protège la mémoire commune de l'écriture par plus d'une méthode à un instant donné (i.e., plus d'un thread à la fois).

Il y a aussi un seul verrou par classe ( appartenant à l'objet Class pour la classe), ainsi les méthodes synchronized static peuvent verrouiller les autres empêchant un accès simultané aux données static sur la base de la classe.

Remarquez que si vous voulez protéger d'autres ressources contre un accès simultanée par des threads multiples, vous pouvez le faire en forçant l'accès à d'autre ressource en passant par des méthodes synchronized.

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Cédric Babault ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 7 8 9 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java