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 

Muni de ce nouveau mot clé la solution est entre nos mains: nous utiliserons simplement le mot clé synchronized pour les méthodes de TwoCounter. L'exemple suivant est le même que le précédent, avec en plus le nouveau mot clé:


//: c14:Sharing2.java
// Utilisant le mot clé synchronized pour éviter
// les accès multiples à une ressource particulière.
// <applet code=Sharing2 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 Sharing2 extends JApplet {
  TwoCounter[] s;
  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;

  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;
    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 synchronized 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 synchronized void synchTest() {
      Sharing2.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 Label("Access Count"));
    p.add(aCount);
    cp.add(p);
  }
  public static void main(String[] args) {
    Sharing2 applet = new Sharing2();
    // Ce n'est pas une applet, donc place le flag et
    // récupère les valeurs de paramètres depuis args:
    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);
  }
} ///:~

Vous noterez que les deux méthodes run() et synchTest() sont synchronized. Si vous synchronizez seulement une des méthodes, alors l'autre est libre d'ignorer l'objet verrouillé et peut être appelé en toute impunité. C'est un point important: chaque méthode qui accède à une ressource partagée critique doit être synchronized ou ça ne fonctionnera pas correctement.

Maintenant un nouveau problème apparaît. Le Watcher ne peut jamais voir [get a peek] ce qui ce passe parce que la méthode run() est entièrement synchronized, et comme run() tourne toujours pour chaque objet le verrou est toujours fermé, synchTest() ne peut jamais être appelé. Vous pouvez le voir parce que accessCount ne change jamais.

Ce que nous aurions voulu pour cet exemple est un moyen d'isoler seulement une partie du code de run(). La section de code que vous voulez isoler de cette manière est appelé une section critique et vous utilisez le mot clé synchronized d'une manière différente pour créer une section critique. Java supporte les sections critiques à l'aide d'un synchronized block; cette fois synchronized est utilisé pour spécifier l'objet sur lequel le verrou est utilisé pour synchronizer le code encapsulé:


synchronized(syncObject) {
  // Ce code ne peut être accéder
  // que par un thread à la fois
}

Avant l'entrée dans le bloc synchronisé, le verrou doit être acquis sur syncObject. Si d'autres threads possède déjà le verrou, l'entrée dans le bloc est impossible jusqu'à ce que le verrou soit libéré.

L'exemple Sharing2 peut être modifié en supprimant le mot clé synchronized de la méthode run() et de mettre à la place un bloc synchronized autour des deux lignes critiques. Mais quel objet devrait être utilisé comme verrou? Celui qui est déjà respecté par synchTest(), qui est l'objet courant (this)! Ainsi la méthode run() modifié ressemble à:


public void run() {
    while (true) {
      synchronized(this) {
        t1.setText(Integer.toString(count1++));
        t2.setText(Integer.toString(count2++));
      }
      try {
        sleep(500);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }

C'est le seul changement qui doit être fait à Sharing2.java, et vous verrez que bien que les compteurs ne soit jamais désynchronisés (d'après ce que Watcher est autorisé à voir d'eux), il y a toujours un accès adéquat fourni au Watcher pendant l'exécution de run().

Bien sûr, toutes les synchronisations dépendent de la diligence du programmeur: chaque morceau de code qui peut accéder à une ressource partagée doit être emballé dans un bloc synchronisé approprié.

Efficacité de la synchronisation

Comme avoir deux méthodes écrivant dans le même morceau de données n'apparaît jamais comme une bonne idée, il semblerait avoir du sens que toutes les méthodes soit automatiquement synchronized et d'éliminer le mot clé synchronized ailleurs. (Bien sûr, l'exemple avec synchronized run() montre que ça ne fonctionnerait pas.) Mais il faut savoir qu'acquérir un verrou n'est pas une opération légère; cela multiplie le coût de l'appel de méthode (c'est à dire entrer et sortir de la méthode, pas exécuter le corps de cette méthode) par au moins quatre fois, et peut être très différent suivant votre implémentation. Donc si vous savez qu'une méthode particulière ne posera pas de problèmes particuliers il est opportun de ne pas utiliser le mot clé synchronized. D'un autre coté, supprimer le mot clé synchronized parce que vous pensez que c'est un goulot d'étranglement pour les performances en espérant qu'il n'y aura pas de collisions est une invitation au désastre.

JavaBeans revisités

Maintenant que vous comprenez la synchronisation, vous pouvez avoir un autre regard sur les Javabeans. Quand vous créez un Bean, vous devez assumez qu'il sera exécuté dans un environnement multithread. Ce qui signifie que:

  1. Autant que possible, toutes les méthodes public d'un Bean devront être synchronized. Bien sûr, cela implique un désagrément lié à l'augmentation de temps d'exécution du à synchronized. Si c'est un problème, les méthodes qui ne poseront pas de problème de sections critiques peuvent être laissées non-synchronized, mais gardez en mémoire que ce n'est pas toujours aussi évident. Les méthodes qui donne l'accès aux attributs ont tendance à être petite (comme getCircleSize() dans l'exemple suivant) et/ou « atomique » en fait, l'appel de méthode exécute un si petit code que l'objet ne peut pas être changé durant l'exécution. Rendre ce type de méthodes non-synchronized ne devrait pas avoir d'effet important sur la vitesse d'exécution de votre programme. Vous devriez de même rendre toutes les méthodes public d'un Bean synchronized et supprimer le mot clé synchronized seulement quand vous savez avec certitude que c'est nécessaire et que ça fera une différence.
  2. Quand vous déclenchez un multicast event à un banc de listeners intéressé par cet événement, vous devez vous assurer que tous les listeners seront ajoutés et supprimés durant le déplacement dans la liste.

Le premier point est assez facile à comprendre, mais le second point exige un petit effort. Considérez l'exemple BangBean.java presenté dans le précédent chapitre. Il évitait la question du multithreading en ignorant le mot clé synchronized (qui n'avait pas été encore introduit) et en rendant l'événement unicast. Voici cet exemple modifié pour fonctionner dans un environnement multi-tâche et utilisant le multicasting pour les événements:


//: c14:BangBean2.java
// Vous devriez écrire vos Beans de cette façon pour qu'ils
// puissent tournés dans un environement multithread.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import com.bruceeckel.swing.*;

public class BangBean2 extends JPanel
    implements Serializable {
  private int xm, ym;
  private int cSize = 20; // Taille du cercle
  private String text = "Bang!";
  private int fontSize = 48;
  private Color tColor = Color.red;
  private ArrayList actionListeners =
    new ArrayList();
  public BangBean2() {
    addMouseListener(new ML());
    addMouseMotionListener(new MM());
  }
  public synchronized int getCircleSize() {
    return cSize;
  }
  public synchronized void
  setCircleSize(int newSize) {
    cSize = newSize;
  }
  public synchronized String getBangText() {
    return text;
  }
  public synchronized void
  setBangText(String newText) {
    text = newText;
  }
  public synchronized int getFontSize() {
    return fontSize;
  }
  public synchronized void
  setFontSize(int newSize) {
    fontSize = newSize;
  }
  public synchronized Color getTextColor() {
    return tColor;
  }
  public synchronized void
  setTextColor(Color newColor) {
    tColor = newColor;
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(Color.black);
    g.drawOval(xm - cSize/2, ym - cSize/2,
      cSize, cSize);
  }
  // C'est un listener multicast, qui est
  // plus typiquement utilisé que l'approche
  // unicast utilisée dans BangBean.java:
  public synchronized void
    addActionListener(ActionListener l) {
    actionListeners.add(l);
  }
  public synchronized void
    removeActionListener(ActionListener l) {
    actionListeners.remove(l);
  }
  // Remarquez qu'elle n'est pas synchronized:
  public void notifyListeners() {
    ActionEvent a =
      new ActionEvent(BangBean2.this,
        ActionEvent.ACTION_PERFORMED, null);
    ArrayList lv = null;
    // Effectue une copie profonde de la liste au cas où
    // quelqu'un ajouterait un listener pendant que nous
    // appelons les listeners:
    synchronized(this) {
      lv = (ArrayList)actionListeners.clone();
    }
    // Apelle toutes les méthodes listeners:
    for(int i = 0; i < lv.size(); i++)
      ((ActionListener)lv.get(i))
        .actionPerformed(a);
  }
  class ML extends MouseAdapter {
    public void mousePressed(MouseEvent e) {
      Graphics g = getGraphics();
      g.setColor(tColor);
      g.setFont(
        new Font(
          "TimesRoman", Font.BOLD, fontSize));
      int width =
        g.getFontMetrics().stringWidth(text);
      g.drawString(text,
        (getSize().width - width) /2,
        getSize().height/2);
      g.dispose();
      notifyListeners();
    }
  }
  class MM extends MouseMotionAdapter {
    public void mouseMoved(MouseEvent e) {
      xm = e.getX();
      ym = e.getY();
      repaint();
    }
  }
  public static void main(String[] args) {
    BangBean2 bb = new BangBean2();
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        System.out.println("ActionEvent" + e);
      }
    });
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        System.out.println("BangBean2 action");
      }
    });
    bb.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e){
        System.out.println("More action");
      }
    });
    Console.run(bb, 300, 300);
  }
} ///:~

Ajouter synchronized aux méthodes est un changement facile. Toutefois, motez que dans addActionListener() et removeActionListener() que les ActionListeners sont maintenant ajoutés et supprimés d'une ArrayList, ainsi vous pouvez en avoir autant que vous voulez.

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