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 

Dans l'exemple ci-dessus, vous pouvez voir que la classe thread est séparée de la classe principale du programme. C'est la solution la plus sensé et c'est relativement facile à comprendre. Il existe toutefois une forme alternative qui vous verrez souvent utiliser, qui n'est pas aussi claire mais qui est souvent plus concise (ce qui augmente probablement sa popularité). Cette forme combine la classe du programme principal avec la classe thread en faisant de la classe du programme principal un thread. Comme pour un programme GUI la classe du programme principal doit hérité soit de Frame soit d'Applet, une interface doit être utilisée pour coller à la nouvelle fonctionnalité. Cette interface est appelée Runnable, elle contient la même méthode de base que Thread. En fait, Thread implémente aussi Runnable, qui spécifie seulement qu'il y a une méthode run()

L'utilisation de la combinaison programme/thread n'est pas vraiment évidente. Quand vous démarrez le programme, vous créez un objet Runnable, mais vous ne démarrez pas le thread. Ceci doit être fait explicitement. C'est ce que vous pouvez voir dans le programme qui suit, qui reproduit la fonctionnalité de Counter2:


//: c14:Counter3.java
// Utilisation de l'interface Runnable pour  
// transformer la classe principale en thread.
// <applet code=Counter3 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Counter3
    extends JApplet implements Runnable {
  private int count = 0;
  private boolean runFlag = true;
  private Thread selfThread = null;
  private JButton
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  private JTextField t = new JTextField(10);
  public void run() {
    while (true) {
      try {
        selfThread.sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      if(runFlag)
        t.setText(Integer.toString(count++));
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(selfThread == null) {
        selfThread = new Thread(Counter3.this);
        selfThread.start();
      }
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    cp.add(t);
    start.addActionListener(new StartL());
    cp.add(start);
    onOff.addActionListener(new OnOffL());
    cp.add(onOff);
  }
  public static void main(String[] args) {
    Console.run(new Counter3(), 300, 100);
  }
} ///:~

Maintenant run() est définie dans la classe, mais reste en sommeil après la fin de init(). Quand vous pressez le bouton start, le thread est créé (si il n'existe pas déjà) dans cette expression obscure:


new Thread(Counter3.this);

Quand une classe a une interface Runnable, cela signifie simplement qu'elle définit une méthode run(), mais n'entraîne rien d'autre de spécial —  contrairement à une classe héritée de thread elle n'a pas de capacité de threading naturelle. Donc pour produire un thread à partir d'un objet Runnable vous devez créer un objet Thread séparé comme ci-dessus, passer l'objet Runnable au constructeur spécial de Thread. Vous pouvez alors appeler start() sur ce thread:


selfThread.start();

L'initialisation usuelle est alors effectuée puis la méthode run() est appelé.

L'aspect pratique de l'interface Runnable est que tout appartient à la même classe. Si vous avez besoin d'accéder à quelque chose, vous le faites simplement sans passer par un objet séparé. Cependant, comme vous avez pu le voir dans le précédent exemple, cet accès est aussi facile en utilisant des classes internes. .

Créer plusieurs threads

Considérons la création de plusieurs threads différents. Vous ne pouvez pas le faire avec l'exemple précédent, vous devez donc revenir à plusieurs classes séparées, héritant de Thread pour encapsuler run(). Il s'agit d'une solution plus générale et facile à comprendre. Bien que l'exemple précédent montre un style de codage que vous rencontrerez souvent, je ne vous le recommande pas pour la plupart des cas car ce style est juste un peu plus confus et moins flexible.

L'exemple suivant reprend la forme des exemples précédents avec des compteurs et des boutons bascules. Mais maintenant toute l'information pour un compteur particulier, le bouton et le champ texte inclus, est dans son propre objet qui hérite de Thread. Tous les champs de Ticker sont private, ce qui signifie que l'implémentation de Ticker peut changer à volonté, ceci inclus la quantité et le type des composants pour acquérir et afficher l'information. Quand un objet Ticker est créé, le constructeur ajoute ces composants au content pane de l'objet externe:


//: c14:Counter4.java
// En conservant votre thread comme une classe distincte
// vous pouvez avoir autant de threads que vous voulez
// <applet code=Counter4 width=200 height=600>
// <param name=size value="12"></applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Counter4 extends JApplet {
  private JButton start = new JButton("Start");
  private boolean started = false;
  private Ticker[] s;
  private boolean isApplet = true;
  private int size = 12;
  class Ticker extends Thread {
    private JButton b = new JButton(color="#004488">"Toggle");
    private JTextField t = new JTextField(10);
    private int count = 0;
    private boolean runFlag = color="#0000ff">true;
    public Ticker() {
      b.addActionListener(new ToggleL());
      JPanel p = new JPanel();
      p.add(t);
      p.add(b);
      // Appelle JApplet.getContentPane().add():
      getContentPane().add(p);
    }
    class ToggleL implements ActionListener {
      public void actionPerformed(ActionEvent e) {
        runFlag = !runFlag;
      }
    }
    public void run() {
      while (true) {
        if (runFlag)
          t.setText(Integer.toString(count++));
        try {
          sleep(100);
        } catch(InterruptedException e) {
          System.err.println("Interrupted");
        }
      }
    }
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for (int i = 0; i < s.length; i++)
          s[i].start();
      }
    }
  }
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    // Obtient le paramètre "size" depuis la page Web
    if (isApplet) {
      String sz = getParameter("size");
      if(sz != null)
        size = Integer.parseInt(sz);
    }
    s = new Ticker[size];
    for (int i = 0; i < s.length; i++)
      s[i] = new Ticker();
    start.addActionListener(new StartL());
    cp.add(start);
  }
  public static void main(String[] args) {
    Counter4 applet = new Counter4();
    // Ce n'est pas une applet, donc désactive le flag et
    // produit la valeur du paramètre depuis les arguments:
    applet.isApplet = false;
    if(args.length != 0)
      applet.size = Integer.parseInt(args[0]);
    Console.run(applet, 200, applet.size * 50);
  }
} ///:~

Ticker contient non seulement l'équipement pour le threading, mais aussi le moyen de contrôler et d'afficher le thread. Vous pouvez créer autant de threads que vous voulez sans créer explicitement les composants graphiques.

On utilise dans Counter4 un tableau d'objets Ticker appelé s. Pour un maximum de flexibilité, la taille du tableau est initialisée en utilisant les paramètres de l'applet donnés dans la page Web. Voici ce à quoi ressemble le paramètre de taille sur la page, inclus dans le tag d'applet:


<param name=size value="20">

Les mots param, name, et value sont tous des mots clés HTML. name est ce à quoi vous ferez référence dans votre programme, et value peut être n'importe quelle chaîne, pas seulement quelque chose qui s'analyse comme un nombre.

Vous remarquerez que la détermination de la taille du tableau s est faite dans la méthode init(), et non comme une définition en ligne de s. En fait, vous ne pouvez pas écrire dans la définition de la classe (en dehors de toutes méthodes):


int size = Integer.parseInt(getParameter("#004488">"size"));
Ticker[] s = new Ticker[size];

Vous pouvez compiler ce code, mais vous obtiendrez une étrange « null-pointer exception » à l'exécution. Cela fonctionne correctement si vous déplacez l'initialisation avec getParameter() dans init(). Le framework de l'applet réalise les opérations nécessaires à l'initialisation pour récupérer les paramètres avant d'entrer dans init().

En plus, ce code est prévu pour être soit une applet, soit une application. Si c'est une application l'argument size est extrait de la ligne de commande (ou une valeur par défaut est utilisée).

Une fois la taille du tableau établie, les nouveaux objets Ticker sont créés; en temps que partie du constructeur, les boutons et champs texte sont ajoutés à l'applet pour chaque Ticker.

Presser le bouton start signifie effectuer une boucle sur la totalité du tableau de Tickers et appeler start() pour chacun d'eux. N'oubliez pas que start() réalise l'initialisation du thread nécessaire et appelle ensuite run() pour chaque thread.

Le listener ToggleL inverse simplement le flag dans Ticker et quand le thread associé en prend note il peut réagir en conséquence.

Un des intérêt de cet exemple est qu'il vous permet de créer facilement un grand jeux de sous-tâches indépendantes et de contrôler leur comportement. Avec ce programme, vous pourrez voir que si on augmente le nombre de sous-tâches, votre machine montrera probablement plus de divergence dans les nombres affichés à cause de la façon dont les threads sont servis.

Vous pouvez également expérimenter pour découvrir combien la méthode sleep(100) est importante dans Ticker.run(). Si vous supprimer sleep(), les choses vont bien fonctionner jusqu'à ce que vous pressiez un bouton. Un thread particulier à un runFlag faux et run() est bloquer dans une courte boucle infinie, qu'il parait difficile d'arrêter au cours du multithreading, la dynamique et la vitesse du programme le fait vraiment boguer. [so the responsiveness and speed of the program really bogs down.]

Threads démons

Un thread démon est un thread qui est supposé proposer un service général en tache de fond aussi longtemps que le programme tourne, mais il n'est pas l'essence du programme. Ainsi, quand tous les threads non démons s'achève, le programme se termine. A l'inverse, si des threads non démons tournent encore le programme ne se termine pas. (Il y a, par exemple, un thread qui tourne main().)

Vous pouvez savoir si un thread est un démon en appelant isDaemon(), et vous pouvez activer ou désactiver la « démonité » [« daemonhood »] d'un thread avec setDaemon(). Si un thread est un démon, alors toutes les threads qu'il créera seront automatiquement des démons.

L'exemple suivant montre des threads démons:


//: c14:Daemons.java
// Un comportement démoniaque

import java.io.*;

class Daemon extends Thread {
  private static final int SIZE = 10;
  private Thread[] t = new Thread[SIZE];
  public Daemon() {
    setDaemon(true);
    start();
  }
  public void run() {
    for(int i = 0; i < SIZE; i++)
      t[i] = new DaemonSpawn(i);
    for(int i = 0; i < SIZE; i++)
      System.out.println(
        "t[" + i + "].isDaemon() = "
        + t[i].isDaemon());
    while(true)
      yield();
  }
}

class DaemonSpawn extends Thread {
  public DaemonSpawn(int i) {
    System.out.println(
      "DaemonSpawn " + i + " started");
    start();
  }
  public void run() {
    while(true)
      yield();
  }
}

public class Daemons {
  public static void main(String[] args)
  throws IOException {
    Thread d = new Daemon();
    System.out.println(
      "d.isDaemon() = " + d.isDaemon());
    // Autorise le thread démon à finir
    // son processus de démarrage
    System.out.println("Press any key");
    System.in.read();
  }
} ///:~

Le thread Daemon positionne son flag démon à « vrai », il engendre alors un groupe d'autre threads pour montrer qu'ils sont aussi des démons. Il tombe alors dans une boucle infinie qui appelle yield() pour rendre le contrôle aux autres processus. Dans une première version de ce programme, la boucle infinie incrémentait un compteur int, mais cela semble entraîner un arrêt du programme. Utiliser yield() rend le programme plutôt nerveux.

Il n'y a rien pour empêcher le programme de se terminer une fois que main() a fini sont travail, puisqu'il n'y a plus que des threads démons qui tournent. Vous pouvez donc voir le résultat du démarrage de tous les threads démons, on appelle read sur System.in pour que le programme attende qu'une touche soit pressée avant de s'arrêter. Sans cela vous ne voyez qu'une partie des résultats de la création des threads démons. (Essayez de remplacer l'appel à read() par des appels à sleep() de différents tailles pour voir ce qui se passe.)

Partager des ressources limitées

Vous pouvez penser un programme à une seule thread comme une seule entité se déplaçant dans votre espace problème et n'effectuant qu'une seule chose à la fois. Puisqu'il y a seulement une entité, vous n'avez jamais penser au problème de deux entité essayant d'utiliser la même ressource en même temps, comme deux personnes essayant de se garer sur la même place, passer la même porte en même temps, ou encore parler en même temps.

Avec le multithreading, les chose ne sont plus seules, mais vous avez maintenant la possibilité d'avoir deux ou trois threads essayant d'utiliser la même ressource limitée à la fois. Les collisions sur une ressource doivent être évitées ou alors vous aurez deux threads essayant d'accéder au même compte bancaire en même temps, imprimer sur la même imprimante, ou ajuster la même soupape, etc...

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