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 

wait() est typiquement utilisé quand vous êtes arrivé à un point où vous attendez qu'une autre condition, sous le contrôle de forces extérieures à votre thread, change et que vous ne voulez pas attendre activement à l'intérieur du thread. Donc wait() vous autorise à mettre votre thread en sommeil en attendant que le monde change, et c'est seulement quand un notify() ou notifyAll() arrive que le thread se réveille et controle les changements. Ainsi, on dispose d'un moyen de synchronization entre les threads.

Bloqué sur I/O

Si un flux est en attente de l'activité d'une I/O, il se bloquera automatiquement. Dans la portion suivante de l'exemple, les deux classes travaillent avec les objets génériques Reader et Writer, mais dans le framework de test un piped stream sera créé afin de permettre au deux threads de se passer des données de façon sûre (ce qui est le but des piped streams).

Le Sender place des données dans le Writer et s'endort pour un temps tiré au hasard. Cependant Receiver n'a pas de sleep(), suspend(), ou wait(). Mais quand il appelle read() il se bloque automatiquement quand il n'y a pas d'autre données.


///:Continuing
class Sender extends Blockable { color="#009900">// envoie
  private Writer out;
  public Sender(Container c, Writer out) {
    super(c);
    this.out = out;
  }
  public void run() {
    while(true) {
      for(char c = 'A'; c <= 'z'; c++) {
        try {
          i++;
          out.write(c);
          state.setText("Sender sent: "
            + (char)c);
          sleep((int)(3000 * Math.random()));
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        } catch(IOException e) {
          System.err.println("IO problem");
        }
      }
    }
  }
}

class Receiver extends Blockable {
  private Reader in;
  public Receiver(Container c, Reader in) {
    super(c);
    this.in = in;
  }
  public void run() {
    try {
      while(true) {
        i++; // Montre que peeker est en vie
        // Bloque jusqu'à ce que les caractères soient là:
        state.setText("Receiver read: "
          + (char)in.read());
      }
    } catch(IOException e) {
      System.err.println("IO problem");
    }
  }
} ///:Continued

Les deux classes placent également des informations dans leurs champs state et change i afin que le Peeker puisse voir que le thread tourne.

Tester

La classe principale de l'applet est étonnamment simple parce la majorité du travail a été mis dans le framework Blockable. En fait, un tableau d'objets Blockable est créé, et puisque chacun est un thread, il réalise leur propre activité quand vous pressez le bouton « start ». Il y a aussi un bouton et une clause actionPerformed() pour stopper tout les objets Peeker, qui donne une démonstration de de l'alternative à la méthode dépréciée stop() de Thread.

Pour établir la connexion entre les objets Sender et Receiver, un PipedWriter et un PipedReader sont créés. Notez que le PipedReader in doit être connecté au PipedWriter out via un argument du constructeur. Après ça, les données placées dans out peuvent être extraites de in, comme si elles passaient dans un tube (d'où le nom) ([NDT: un pipe est un tube en anglais]). Les objets in et out sont alors passés respectivement aux constructeurs de Receiver et Sender, qui les traitent comme des objets Reader et Writer (ils sont upcast).

Le tableau de références Blockable b n'est pas initialisé à son point de définition parce que les piped streams ne peuvent pas être établis avant cette définition (l'utilisation du bloc try évite cela).


///:Continuing
/////////// Test de tout ///////////
public class Blocking extends JApplet {
  private JButton
    start = new JButton("Start"),
    stopPeekers = new JButton("#004488">"Stop Peekers");
  private boolean started = false;
  private Blockable[] b;
  private PipedWriter out;
  private PipedReader in;
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for(int i = 0; i < b.length; i++)
          b[i].start();
      }
    }
  }
  class StopPeekersL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      // Demonstration de la meilleure
      // alternative à Thread.stop():
      for(int i = 0; i < b.length; i++)
        b[i].stopPeeker();
    }
  }
  public void init() {
     Container cp = getContentPane();
     cp.setLayout(new FlowLayout());
     out = new PipedWriter();
    try {
      in = new PipedReader(out);
    } catch(IOException e) {
      System.err.println("PipedReader problem");
    }
    b = new Blockable[] {
      new Sleeper1(cp),
      new Sleeper2(cp),
      new SuspendResume1(cp),
      new SuspendResume2(cp),
      new WaitNotify1(cp),
      new WaitNotify2(cp),
      new Sender(cp, out),
      new Receiver(cp, in)
    };
    start.addActionListener(new StartL());
    cp.add(start);
    stopPeekers.addActionListener(
      new StopPeekersL());
    cp.add(stopPeekers);
  }
  public static void main(String[] args) {
    Console.run(new Blocking(), 350, 550);
  }
} ///:~

Dans init(), notez la boucle qui parcourt la totalité du tableau et ajoute les champs state et peeker.status à la page.

Quand les threads Blockable sont initialement créés, chacune crée et démarre automatiquement son propre Peeker. Donc vous verrez les Peekers tournés avant que les threads Blockable ne démarrent. C'est important, puisque certains des Peekers seront bloqués et stoperont quand les threads Blockable démarreront, et c'est essentiel de voir et de comprendre cet aspect particulier du blocage.

Interblocage [Deadlock]

Puisse que les threads peuvent être bloqués et puisse que les objets peuvent avoir des méthodes synchronized qui empêchent les threads d'accéder à cet objet jusqu'à ce que le verrou de synchronisation soit libéré, il est possible pour un thread de rester coincé attendant un autre thread, qui à son tour attend un autre thread, etc., jusqu'à ce que la chaîne ramène à un thread en attente sur le premier. Vous obtenez une boucle continue de threads s'attendant les uns les autres et aucun ne peut bouger. C'est ce qu'on appelle un interblocage (ou deadlock). Le pire c'est que cela n'arrive pas souvent, mais quand cela vous arrive c'est frustrant à déboguer.

Il n'y a pas de support du langage pour aider à éviter les interblocages; c'est à vous de les éviter en faisant attention à la conception. Ce ne sont pas des mots pour rassurer la personne qui essaie de déboguer un programme générant des inter-blocages.

La dépréciation de stop(), suspend(), resume(), et destroy() en Java 2

Un changement qui a été fait dans Java 2 pour réduire les possibilités d'inter-blocage est la dépréciation des méthodes de Thread&rsquo; stop(), suspend(), resume(), et destroy().

La méthode stop() est dépréciée parce qu'elle ne libère pas les verrous que le thread a acquis, et si les objets sont dans un état inconsistent (« damaged ») les autres threads peuvent les voir et les modifiés dans cet état. Le problème résultant peut être subtil et difficile à détecter. Plutôt que d'utiliser stop(), vous devriez suivre l'exemple de Blocking.java et utiliser un drapeau pour dire au thread quand se terminer en sortant de sa méthode run().

Il existe des situations où un thread se bloque — comme quand il attend une entrée — mais il ne peut pas positionner un drapeau comme il le fait dans Blocking.java. Dans ces cas, vous ne devriez pas utiliser stop(), mais plutôt la méthode interrupt() de Thread pour écrire le code bloquant:


//: c14:Interrupt.java
// L'approche alternative pour utiliser
// stop() quand un thread est bloqué.
// <applet code=Interrupt width=200 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

class Blocked extends Thread {
  public synchronized void run() {
    try {
      wait(); // Bloque
    } catch(InterruptedException e) {
      System.err.println("Interrupted");
    }
    System.out.println("Exiting run()");
  }
}

public class Interrupt extends JApplet {
  private JButton
    interrupt = new JButton("Interrupt");
  private Blocked blocked = new Blocked();
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(interrupt);
    interrupt.addActionListener(
      new ActionListener() {
        public
        void actionPerformed(ActionEvent e) {
          System.out.println("Button pressed");
          if(blocked == null) color="#0000ff">return;
          Thread remove = blocked;
          blocked = null; // pour le libérer
          remove.interrupt();
        }
      });
    blocked.start();
  }
  public static void main(String[] args) {
    Console.run(new Interrupt(), 200, 100);
  }
} ///:~

Le wait() dans Blocked.run() produit le thread bloqué. Quand vous pressez le bouton, la référence blocked est placée à null donc le garbage collector le nettoiera, la méthode interrupt() de l'objet est alors appelée. La première fois que vous pressez le bouton vous verrez le thread sortir, mais ensuite il n'y a plus de thread à tuer donc vous voyez juste que le bouton a été pressé.

Les méthodes suspend() et resume() finissent par être sujettes aux inter-blocages. Quand vous appelez suspend(), le thread cible s'arrête mais il conserve les verrous qu'il a acquis à ce point. Ainsi aucuns autres threads ne peut accéder aux ressources verrouillées jusqu'à ce que le thread soit redémarré. Un thread qui veut redémarrer le thread cible et aussi essaye d'utiliser une des ressources verrouillées produit un inter-blocage. Vous ne devriez pas utiliser suspend() et resume(), mais plutôt mettre un drapeau dans votre classe Thread pour indiqué si le thread devrait être actif ou suspendu. Si le drapeau indique que le thread est suspendu, le thread rentre dans un wait(). Quand le drapeau indique que le thread devrait être redémarré le thread est réactivé avec notify(). Un exemple peut être produit en modifiant Counter2.java. Alors que l'effet est similaire, vous remarquerez que l'organisation du code est assez différente —  des classes internes anonymes sont utilisées pour tous les listeners et le Thread est une classe interne, ce qui rend la programmation légèrement plus convenable puisqu'on élimine certaines complexités nécessaires dans Counter2.java:


//: c14:Suspend.java
// L'approche alternative à l'utilisation de suspend()
// et resume(), qui sont déprecié dans Java 2.
// <applet code=Suspend width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Suspend extends JApplet {
  private JTextField t = new JTextField(10);
  private JButton
    suspend = new JButton("Suspend"),
    resume = new JButton("Resume");
  private Suspendable ss = new Suspendable();
  class Suspendable extends Thread {
    private int count = 0;
    private boolean suspended = color="#0000ff">false;
    public Suspendable() { start(); }
    public void fauxSuspend() {
      suspended = true;
    }
    public synchronized void fauxResume() {
      suspended = false;
      notify();
    }
    public void run() {
      while (true) {
        try {
          sleep(100);
          synchronized(this) {
            while(suspended)
              wait();
          }
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
        t.setText(Integer.toString(count++));
      }
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    suspend.addActionListener(
      new ActionListener() {
        public
        void actionPerformed(ActionEvent e) {
          ss.fauxSuspend();
        }
      });
    cp.add(suspend);
    resume.addActionListener(
      new ActionListener() {
        public
        void actionPerformed(ActionEvent e) {
          ss.fauxResume();
        }
      });
    cp.add(resume);
  }
  public static void main(String[] args) {
    Console.run(new Suspend(), 300, 100);
  }
} ///:~

Le drapeau suspended dans Suspendable est utilisé pour activer ou désactiver la suspension. Pour suspendre, le drapeau est placé à true en appelant fauxSuspend() et ceci est détecté dans run(). Le wait(), comme décrit plus haut dans ce chapitre, doit être synchronized afin qu'il ait le verrou de l'objet. Dans fauxResume(), le drapeau suspended est placé à false et notify() est appelé — puisque ceci reveille wait() dans une clause synchronized la méthode fauxResume() doit aussi être synchronized afin qu'elle acquiert le verrou avant d'appeler notify() (ainsi le verrou est libre pour que le wait() se réveille avec). Si vous suivez le style montré dans ce programme vous pouvez éviter d'utiliser suspend() et resume().

La méthode destroy() de Thread n'a jamais été implémenter; c'est comme un suspend() qui ne peut pas être réactivé, donc elle a les mêmes problèmes d'inter-blocage que suspend(). Toutefois, ce n'est pas une méthode dépréciée et elle devrait être implémentée dans une future version de Java (après la 2) pour des situations spéciales où le risque d'inter-blocage est acceptable.

Vous devez vous demander pourquoi ces méthodes, maintenant dépréciées, étaient incluses dans Java dans un premier temps. Il semblerait admissible qu'une erreur significative soit simplement supprimée (et donne encore un autre coup aux arguments pour l'exceptionnel conception et l'infaillibilité claironné par les commerciaux de Sun). La partie réconfortante à propos des changements est que cela indique clairement que ce sont les techniciens et non les commerciaux qui dirige le show — ils découvrent un problème et ils le fixent. Je trouve cela beaucoup plus promettant et encourageant que de laisser le problème parce que « fixer le problème serait admettre une erreur. » Cela signifie que Java continuera à évoluer, même si cela signifie une petite perte de confort pour les programmeurs Java. Je préfère accepter cet inconvénient plutôt que de voir le langage stagné.

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