IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Penser en Java

2nde édition


précédentsommairesuivant

XVI. Les threads multiples

Les objets permettent une division d'un programme en sections indépendantes. Souvent, nous avons aussi besoin de découper un programme en unités d'exécution indépendantes.

Chacune de ces unités d'exécution est appelée un thread, et vous programmez comme si chaque thread s'exécutait par lui-même et avait son propre CPU. Un mécanisme sous-jacent s'occupe de diviser le temps CPU pour vous, mais en général vous n'avez pas besoin d'y penser, c'est ce qui fait de la programmation multithread une tâche beaucoup plus facile.

Un processus est un programme s'exécutant dans son propre espace d'adressage. Un système d'exploitation multitâche est capable d'exécuter plusieurs processus (programme) en même temps, en faisant comme si chacun d'eux s'exécutait de façon indépendante, en accordant périodiquement des cycles CPU à chaque processus. Un thread est un flot de contrôle séquentiel à l'intérieur d'un processus. Un processus peut contenir plusieurs threads s'exécutant en concurrence.

Il existe plusieurs utilisations possibles du multithreading, mais en général, vous aurez une partie de votre programme attachée à un événement ou une ressource particulière, et vous ne voulez pas que le reste de votre programme dépende de ça. Vous créez donc un thread associé à cet événement ou ressource et laissez celui-ci s'exécuter indépendamment du programme principal. Un bon exemple est celui d'un bouton « quitter » - vous ne voulez pas être obligé de vérifier le bouton quitter dans chaque partie de code que vous écrivez dans votre programme, mais vous voulez que le bouton quitter réponde comme si vous le vérifiiez régulièrement. En fait, une des plus immédiatement indiscutables raisons d'être du multithreading est de produire des interfaces utilisateur dynamiques. [responsive user interface]

XVI-A. Interfaces utilisateur dynamiques [Responsive user interfaces]

Comme point de départ, considérons un programme qui contient des opérations nécessitant beaucoup de temps CPU finissant ainsi par ignorer les entrées de l'utilisateur et répondrait mal. Celle-ci, une combinaison applet/application, affichera simplement le résultat d'un compteur actif [running counter] :

 
Sélectionnez
//: c14:Counter1.java
// Une interface utilisateur sans répondant.
// <applet code=Counter1 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import com.bruceeckel.swing.*;

public class Counter1 extends JApplet {
  private int count = 0;
  private JButton
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  private JTextField t = new JTextField(10);
  private boolean runFlag = true;
  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 void go() {
    while (true) {
      try {
        Thread.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) {
      go();
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  public static void main(String[] args) {
    Console.run(new Counter1(), 300, 100);
  }
} ///:~

À ce niveau, le code Swing et de l'applet devrait être raisonnablement familier depuis le titre XV. La méthode go() est celle dans laquelle le programme reste occupé : il met la valeur courante de count dans le JTextField t, avant d'incrémenter count.

La boucle infinie à l'intérieur de go() appelle sleep(). sleep() doit être associé avec un objet Thread, ce qui montre que toute application a un thread qui lui est associé. (En effet, Java est basé sur des threads et il y en a toujours qui tournent avec votre application.) Donc, que vous utilisiez explicitement les threads ou non, vous pouvez accéder au thread courant utilisé par votre programme avec Thread et la méthode static sleep().

Notez que sleep() peut déclencher une InterruptedException, alors que le déclenchement d'une telle exception est considéré comme un moyen hostile d'arrêter un thread et devrait être découragé. (Encore une fois les exceptions sont destinées aux conditions exceptionnelles et non pour le contrôle du flot normal.) L'interruption d'un thread en sommeil sera incluse dans une future fonctionnalité du langage.

Quand le bouton start est pressé, go() est appelé. En examinant go(), vous pouvez naïvement pensé (comme je le fais) que cela autorisera le multithreading parce qu'elle se met en sommeil [it gœs to sleep]. C'est le cas, tant que la méthode n'est pas en sommeil, tout se passe comme si le CPU pouvait être occupé à gérer les autres boutons. [=That is, while the method is asleep, it seems like the CPU could be busy monitoring other button presses. ] Mais il s'avère que le vrai problème est que go() ne s'achève jamais, puisqu'il s'agit d'une boucle infinie, ce qui signifie que actionPerformed() ne s'achève jamais. Comme vous êtes bloqué dans actionPerformed() par la première touche appuyée, le programme ne peut pas capturer les autres événements. (Pour sortir, vous devez tuer le processus ; le moyen le plus facile de le faire est de presser les touches Control-C dans la console, si vous avez lancé le programme depuis la console. Si vous l'avez démarré dans un navigateur, vous devez fermer la fenêtre du navigateur.)

Le problème de base ici est que go() doit continuer de réaliser ses opérations, et dans le même temps elle doit retourner à la fonction appelante de façon à ce que actionPerformed() puisse se terminer et que l'interface utilisateur puisse continuer à répondre à l'utilisateur. Mais une méthode conventionnelle comme go() ne peut pas continuer et dans le même temps rendre le contrôle au reste du programme. Cela à l'air d'une chose impossible à accomplir, comme si le CPU devait être à deux endroits à la fois, mais c'est précisément l'illusion que les threads permettent.

Le modèle des threads (et son support de programmation Java) est une commodité de programmation pour simplifier le jonglage entre plusieurs opérations simultanées dans un même programme. Avec les threads, le temps CPU sera éclaté et distribué entre les différents threads. Chaque thread a l'impression d'avoir constamment le CPU pour lui tout seul, mais le temps CPU est distribué entre les différents threads. L'exception à cela est si votre programme tourne sur plusieurs CPU. Mais ce qui est intéressant dans le threading c'est de permettre une abstraction par rapport à cette couche, votre code n'a pas besoin de savoir s'il tournera sur un ou plusieurs CPU. Ainsi, les threads sont un moyen de créer de façon transparente des programmes portables.

Le threading réduit l'efficacité informatique, mais la nette amélioration dans la conception des programmes, la gestion de ressources [resource balancing], et le confort d'utilisation est souvent valable. Bien sûr, si vous avez plus d'un CPU, alors le système d'exploitation pourra décider quel CPU attribuer à quel jeu de threads ou attribuer un seul thread et la totalité du programme pourra s'exécuter beaucoup plus vite. Le multitâche et le multithreading tendent à être l'approche la plus raisonnable pour utiliser les systèmes multiprocesseurs.

XVI-A-1. Héritage de Thread

Le moyen le plus simple de créer un thread est d'hériter de la classe Thread, qui a toutes les connexions nécessaires pour créer et faire tourner les threads. La plus importante méthode de Thread est run(), qui doit être redéfinie pour que le thread fasse ce que vous lui demandez. Ainsi, run(), contient le code qui sera exécuté « simultanément » avec les autres threads du programme.

L'exemple suivant crée un certain nombre de threads dont il garde la trace en attribuant à chaque thread un numéro unique, généré à l'aide d'une variable static. La méthode run() du Thread est redéfinie pour décrémenter le compteur à chaque fois qu'elle passe dans sa boucle et se termine quand le compteur est à zéro (au moment où run() retourne, le thread se termine).

 
Sélectionnez
//: c14:SimpleThread.java
//  Un exemple très simple de Threading.

public class SimpleThread extends Thread {
  private int countDown = 5;
  private static int threadCount = 0;
  private int threadNumber = ++threadCount;
  public SimpleThread() {
    System.out.println("Making " + threadNumber);
  }
  public void run() {
    while(true) {
      System.out.println("Thread " + 
        threadNumber + "(" + countDown + "#004488">")");
      if(--countDown == 0) return;
    }
  }
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++)
      new SimpleThread().start();
    System.out.println("All Threads Started");
  }
} ///:~

Une méthode run() a toujours virtuellement une sorte de boucle qui continue jusqu'à ce que le thread ne soit plus nécessaire, ainsi vous pouvez établir la condition sur laquelle arrêter cette boucle (ou, comme dans le cas précédent, simplement retourner de run()). Souvent, run() est transformé en une boucle infinie, ce qui signifie qu'à moins qu'un facteur extérieur ne cause la terminaison de run(), elle continuera pour toujours.

Dans main() vous pouvez voir un certain nombre de threads créés et lancés. La méthode start() de la classe Thread procède à une initialisation spéciale du thread et appelle ensuite run(). Les étapes sont donc : le constructeur est appelé pour construire l'objet, puis start() configure le thread et appelle run(). Si vous n'appelez pas start() (ce que vous pouvez faire dans le constructeur, si c'est approprié) le thread ne sera jamais démarré.

La sortie d'une exécution de ce programme (qui peut être différente d'une exécution à l'autre) est :

 
Sélectionnez
Making 1
Making 2
Making 3
Making 4
Making 5
Thread 1(5)
Thread 1(4)
Thread 1(3)
Thread 1(2)
Thread 2(5)
Thread 2(4)
Thread 2(3)
Thread 2(2)
Thread 2(1)
Thread 1(1)
All Threads Started
Thread 3(5)
Thread 4(5)
Thread 4(4)
Thread 4(3)
Thread 4(2)
Thread 4(1)
Thread 5(5)
Thread 5(4)
Thread 5(3)
Thread 5(2)
Thread 5(1)
Thread 3(4)
Thread 3(3)
Thread 3(2)
Thread 3(1)

Vous aurez remarqué que nulle part dans cet exemple sleep() n'est appelé, mais malgré cela la sortie indique que chaque thread a eu une portion de temps CPU dans lequel s'exécuter. Cela montre que sleep(), bien que relié à l'existence d'un thread pour pouvoir s'exécuter, n'est pas impliqué dans l'activation et la désactivation du threading. C'est simplement une autre méthode [Ndt de la classe Thread].

Vous pouvez aussi voir que les threads ne sont pas exécutés dans l'ordre où ils sont créés. En fait, l'ordre dans lequel le CPU s'occupe d'un ensemble de threads donné est indéterminé, à moins que vous ne rentriez dans l'ajustement des priorités en utilisant la méthode setPriority() de Thread.

Quand main() crée les objets Thread, elle ne capture aucune des références sur ces objets. Un objet ordinaire serait la proie rêvée pour le garbage collector, mais pas un Thread. Chaque Thread « s'enregistre » lui-même, il y a alors quelque part une référence sur celui-ci donc le garbage collector ne peut pas le détruire.

XVI-A-2. Threading pour une interface réactive

Il est maintenant possible de résoudre le problème de Counter1.java avec un thread. Le truc est de placer une sous-tâche  -  est la boucle de la méthode go() -  dans la méthode run() du thread. Quand l'utilisateur presse le bouton start, le thread est démarré, mais quand la création du thread est terminée, donc que le thread tourne, le principal travail du programme (chercher et répondre aux événements de l'interface utilisateur) peut continuer. Voici la solution :

 
Sélectionnez
//: c14:Counter2.java
// Une interface utilisateur réactive grâce aux threads.
// <applet code=Counter2 width=300 height=100>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

public class Counter2 extends JApplet {
  private class SeparateSubTask color="#0000ff">extends Thread {
    private int count = 0;
    private boolean runFlag = color="#0000ff">true;
    SeparateSubTask() { start(); }
    void invertFlag() { runFlag = !runFlag; }
    public void run() {
      while (true) {
       try {
        sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
       if(runFlag) 
         t.setText(Integer.toString(count++));
      }
    }
  } 
  private SeparateSubTask sp = null;
  private JTextField t = new JTextField(10);
  private JButton 
    start = new JButton("Start"),
    onOff = new JButton("Toggle");
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp == null)
        sp = new SeparateSubTask();
    }
  }
  class OnOffL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(sp != null)
        sp.invertFlag();
    }
  }
  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 Counter2 (), 300, 100);
  }
} ///:~

Counter2 est un programme simple, son seul travail est de préparer et maintenir l'interface utilisateur. Mais maintenant, quand l'utilisateur presse le bouton start, le code gérant l'événement n'appelle plus de méthode. Au lieu de ça un thread de la classe SeparateSubTask est créé et la boucle d'événements de Counter2 continue.

La classe SeparateSubTask est une simple extension de Thread avec un constructeur qui lance le thread en appelant start(), et un run() qui contient essentiellement le code de « go() » de Counter1.java.

La classe SeparateSubTask étant une classe interne, elle peut accéder directement au JTextField t dans Counter2 ; comme vous pouvez le voir dans run(). SeparateSubTask peut accéder au champ t sans permission spéciale malgré que ce champ soit déclaré private dans la classe externe  -  il est toujours bon de rendre les champs « aussi privés que possible » de façon à ce qu'ils ne soient pas accidentellement changés à l'extérieur de votre classe.

Quand vous pressez le bouton onOff, runFlag est inversé dans l'objet SeparateSubTask. Ce thread (quand il regarde le flag) peut se démarrer ou s'arrêter tout seul. Presser le bouton onOff produit un temps de réponse apparent. Bien sûr, la réponse n'est pas vraiment instantanée contrairement à ce qui se passe sur un système fonctionnant par interruptions. Le compteur ne s'arrête que lorsque le thread a le CPU et s'aperçoit que le flag a changé.

Vous pouvez voir que la classe interne SeparateSubTaskest private, ce qui signifie que l'on peut donner à ces champs et méthodes l'accès par défaut (excepté pour run() qui doit être public puisqu'elle est public dans la classe de base). La classe interne private ne peut être accédée par personne sauf Counter2, les deux classes sont ainsi fortement couplées. Chaque fois que vous constatez que deux classes apparaissent comme fortement couplées, vous remarquerez le gain en codage et en maintenance que les classes internes peuvent apporter.

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ée et c'est relativement facile à comprendre. Il existe toutefois une forme alternative que 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ériter 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 :

 
Sélectionnez
//: 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éé (s'il n'existe pas déjà) dans cette expression obscure :

 
Sélectionnez
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 :

 
Sélectionnez
selfThread.start();

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

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.

XVI-A-3. 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 inclut 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 :

 
Sélectionnez
//: 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 :

 
Sélectionnez
<param name=size value="20">

Les mots param, name, et value sont tous des mots-clefs 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) :

 
Sélectionnez
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 tant 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êts de cet exemple est qu'il vous permet de créer facilement un grand jeu 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 supprimez sleep(), les choses vont bien fonctionner jusqu'à ce que vous pressiez un bouton. Un thread particulier à un runFlag faux et run() est bloqué 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.]

XVI-A-4. Threads démons

Un thread démon est un thread qui est supposé proposer un service général en tâche 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èvent, le programme se termine. À 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 :

 
Sélectionnez
//: 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'autres 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érentes tailles pour voir ce qui se passe.)

XVI-B. 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és 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 choses 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.

Considérons une variation sur les compteurs déjà utilisés 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 s'ils restent toujours les mêmes. Ceci apparaît comme une activité inutile puisqu'en regardant le code il apparaît évident que les compteurs resteront toujours les mêmes. Mais c'est là que la surprise arrive. Voici la première version du programme :

 
Sélectionnez
//: 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 équivalents. 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 un 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 tâche à l'autre.

La méthode synchTest() effectue l'activité apparemment inutile de vérifier si count1 est équivalent à count2 ; s'ils ne sont pas équivalents, 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 s'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'e ny comprends rien...]

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

 
Sélectionnez
<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 :

 
Sélectionnez
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. À 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é 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 thread 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édée 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 ressource, 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.

XVI-B-1. Comment Java partage les ressources

Java possède un support intégré pour prévoir les collisions sur un type de ressources : la mémoire 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 :

 
Sélectionnez
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 appelez une méthode synchronized, cet objet est verrouillé et aucune autre méthode synchronized de cet objet ne peut être appelée 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é par des threads multiples, vous pouvez le faire en forçant l'accès à d'autres ressources en passant par des méthodes synchronized.

Muni de ce nouveau mot-clef la solution est entre nos mains : nous utiliserons simplement le mot-clef 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-clef :

 
Sélectionnez
//: c14:Sharing2.java
// Utilisant le mot-clef 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 synchronisez seulement une des méthodes, alors l'autre est libre d'ignorer l'objet verrouillé et peut être appelée 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 se 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ée une section critique et vous utilisez le mot-clef 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 synchroniser le code encapsulé :

 
Sélectionnez
synchronized(syncObject) {
  // Ce code ne peut être accédé 
  // 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èdent 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-clef 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ée ressemble à :

 
Sélectionnez
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 doive être fait à Sharing2.java, et vous verrez que bien que les compteurs ne soient 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é.

XVI-B-1-a. 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-clef 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, pour 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-clef synchronized. D'un autre côté, supprimer le mot-clef 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.

XVI-B-2. 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 assumer 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 dû à 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 donnent l'accès aux attributs ont tendance à être petites (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-clef 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és 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 présenté dans le précédent chapitre. Il évitait la question du multithreading en ignorant le mot-clef synchronized (qui n'avait pas été encore présenté) et en rendant l'événement unicast. Voici cet exemple modifié pour fonctionner dans un environnement multitâche et utilisant le multicasting pour les événements :

 
Sélectionnez
//: c14:BangBean2.java
// Vous devriez écrire vos Beans de cette façon pour qu'ils 
// puissent tourner dans un environnement 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, notez 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.

La méthode paintComponent() n'est pas non plus synchronized. Décider de synchroniser les méthodes surchargées n'est pas aussi clair que quand vous ajoutez vos propres méthodes. Dans cet exemple, il semblerait que peint() fonctionne correctement qu'il soit synchronized ou non. Mais les points que vous devez considérer sont :

  1. Cette méthode modifie-t-elle l'état des variables « critiques » de l'objet. Pour découvrir si les variables sont « critiques » vous devez déterminer si elles seront lues ou modifiées par d'autres threads dans le programme. (Dans ce cas, la lecture ou la modification sont pratiquement toujours réalisées par des méthodes synchronized, ainsi vous pouvez examiner juste celles-là.) Dans le cas de paint(), aucune modification n'apparaît ;
  2. Cette méthode dépend-elle de l'état de ces variables « critiques » ? Si une méthode synchronized modifie une variable que votre méthode utilise, alors vous devriez vouloir mettre aussi votre méthode synchronized. En vous basant là-dessus, vous devriez observer que cSize est changé par des méthodes synchronized et en conséquence paint() devrait être synchronized. Ici, pourtant, vous pouvez vous demander « Quelle serait la pire chose qui arriverait si cSize changeait durant paint() ? » Quand vous voyez que ce n'est rien de trop grave, et un effet transitoire en plus, vous pouvez décider de laisser paint() un-synchronized pour éviter le temps supplémentaire de l'appel de méthode synchronized ;
  3. Un troisième indice est de noter si la version de la classe de base de paint() est synchronized, ce qui n'est pas le cas. Ce n'est pas un argument hermétique, juste un indice. Dans ce cas, par exemple, un champ qui est changé via des méthodes synchronizes (il s'agit de cSize) a été mélangé dans la formule de paint() et devrait avoir changé la situation. Notez, cependant, que synchronized n'est pas hérité -  c'est-à-dire que si une méthode est synchronized dans une classe de base, alors il n'est pas automatiquement synchronized dans la version redéfinie de la classe dérivée.

Le code de test dans TestBangBean2 a été modifié depuis celui du chapitre précédent pour démontrer la possibilité de multicast de BangBean2 en ajoutant des listeners supplémentaires.

XVI-C. Blocage [Blocking]

Un thread peut être dans un des quatre états suivants :

  1. Nouveau (New): l'objet thread a été créé, mais il n'a pas encore été démarré donc il ne peut pas tourner ;
  2. Runnable: cela signifie qu'un thread peut tourner quand le mécanisme de découpage du temps a des cycles CPU disponibles pour le thread. Ainsi, le thread pourra ou non tourner, mais il n'y a rien pour l'empêcher de tourner si le scheduler peut l'organiser ; il n'est ni mort ni bloqué ;
  3. Mort (Dead): la façon normale pour un thread de mourir est de terminer sa méthode run(). Vous pouvez également appelé stop(), mais cela déclenche une exception qui est une sous-classe de Error (ce qui signifie que vous n'êtes pas obligé de placer l'appel dans un bloc try). Souvenez-vous que déclencher une exception devrait être un événement spécial et non une partie de l'exécution normale du programme ; ainsi l'utilisation de stop() est dépréciée dans Java 2. Il y a aussi une méthode destroy() (qui n'a jamais été implémentée) qui vous ne devriez jamais utiliser si vous pouvez l'éviter puisqu'elle est radicale et ne libère pas les verrous de l'objet ;
  4. Bloqué (Blocked): le thread pourrait tourner, mais quelque chose l'en empêche. Tant qu'un thread est dans un état bloqué le scheduler le sautera et ne lui donnera pas de temps CPU. Jusqu'à ce que le thread repasse dans un état runnable il ne réalisera aucune opération.

Passer à l'état bloqué

L'état bloqué est le plus intéressant, et nécessite un examen. Un thread peut devenir bloqué pour cinq raisons :

  1. Vous avez mis le thread à dormir en appelant sleep(milliseconds), auquel cas il ne tournera pas pendant le temps spécifié ;
  2. Vous avez suspendu l'exécution du thread avec suspend(). Il ne redeviendra pas runnable avant que le thread ne reçoive le message resume() (ces possibilités sont dépréciées dans Java2, et seront examinées plus tard) ;
  3. Vous avez suspendu l'exécution du thread avec wait(). Il ne redeviendra pas runnable tant qu'il n'aura pas reçu l'un des messages notify() ou notifyAll(). (Oui, c'est comme le point 2, mais il y a une distinction qui sera vue plus tard.) ;
  4. Le thread attend la fin d'une I/O ;
  5. Le thread essaye d'appeler une méthode synchronized sur un autre objet, et le verrou de cet objet n'est pas libre.

Vous pouvez aussi appeler yield() (une méthode de la classe Thread) pour volontairement donner le CPU afin que d'autres threads puissent tourner. Toutefois, il se passe la même chose si le scheduler décide que votre thread a eu assez de temps et saute à un autre thread. En fait, rien n'empêche le scheduler de partir de votre thread et de donner du temps à un autre thread. Quand un thread est bloqué, c'est qu'il y a une raison pour qu'il ne puisse continuer à tourner.

L'exemple suivant montre les cinq façons de devenir bloqué. Il existe en intégralité dans un seul fichier appelé Blocking.java, mais il sera examiné ici par morceau. (Vous remarquerez les tags « Continued » et « Continuing » qui permettent à l'outil d'extraction de recoller les morceaux.)

Étant donné que cet exemple montre des méthodes dépréciées, vous obtiendrez des messages de dépréciation lors de la compilation.

D'abord le programme de base :

 
Sélectionnez
//: c14:Blocking.java
// Démontre les différentes façons de 
// bloquer un thread.
// <applet code=Blocking width=350 height=550>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import com.bruceeckel.swing.*;

//////////// Le framework de base ///////////
class Blockable extends Thread {
  private Peeker peeker;
  protected JTextField state = new JTextField(30);
  protected int i;
  public Blockable(Container c) {
    c.add(state);
    peeker = new Peeker(this, c);
  }
  public synchronized int read() { return i; }
  protected synchronized void update() {
    state.setText(getClass().getName()
      + " state: i = " + i);
  }
  public void stopPeeker() { 
    // peeker.stop(); Déprécié en Java 1.2
    peeker.terminate(); // L'approche préférée
  }
}

class Peeker extends Thread {
  private Blockable b;
  private int session;
  private JTextField status = new JTextField(30);
  private boolean stop = false;
  public Peeker(Blockable b, Container c) {
    c.add(status);
    this.b = b;
    start();
  }
  public void terminate() { stop = color="#0000ff">true; }
  public void run() {
    while (!stop) {
      status.setText(b.getClass().getName()
        + " Peeker " + (++session)
        + "; value = " + b.read());
       try {
        sleep(100);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
} ///:Continued

La classe Blockable est destinée à être la classe de base pour toutes les classes de cet exemple qui démontre le blocking. Un objet Blockable contient un JTextField appelé state qui est utilisé pour afficher l'information sur l'objet. La méthode qui affiche cette information est update(). Vous pouvez voir qu'elle utilise getClass().getName() pour produire le nom de la classe plutôt que de l'afficher directement ; c'est parce que update() ne peut pas connaître le nom exact de la classe pour laquelle elle est appelée, puisque ce sera une classe dérivée de Blockable.

L'indicateur de changement dans Blockable est un int i, qui sera incrémenté par la méthode run() de la classe dérivée.

Il y a un thread de la classe Peeker qui est démarré pour chaque objet Blockable, et le travail de Peeker est de regarder son objet Blockable associé pour voir les changements de i en appelant read() et en les reportant dans son status JTextField. C'est important : notez que read() et update() sont toutes les deux synchronized, ce qui signifie qu'elles nécessitent que le verrou de l'objet soit libre.

XVI-C-1-a. Dormant (Sleeping)

Le premier test de ce programme est avec sleep() :

 
Sélectionnez
///:Continuing
///////////// Bloqué via sleep() ///////////
class Sleeper1 extends Blockable {
  public Sleeper1(Container c) { super(c); }
  public synchronized void run() {
    while(true) {
      i++;
      update();
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
}
  
class Sleeper2 extends Blockable {
  public Sleeper2(Container c) { super(c); }
  public void run() {
    while(true) {
      change();
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
} ///:Continued

Dans Sleeper1 la méthode run() est entièrement synchronized. Vous verrez que le Peeker associé avec cet objet tournera tranquillement jusqu'à vous démarriez le thread, ensuite le Peeker sera gelé. C'est une forme de blocage : puisque Sleeper1.run() est synchronized, et une fois que le thread démarre dans run(), la méthode ne redonne jamais le verrou de l'objet et le Peeker est bloqué.

Sleeper2 fournit une solution en rendant run() un-synchronized. Seule la méthode change() est synchronized, ce qui signifie que tant que run() est dans sleep(), le Peeker peut accéder à la méthode synchronized dont il a besoin, nommée read(). Ici vous verrez que Peeker continue à tourner quand vous démarrez le thread Sleeper2.

XVI-C-1-b. Suspension et reprise

La partie suivante de l'exemple introduit le concept de suspension. La classe Thread a une méthode suspend() pour arrêter temporairement le thread et resume() qui le redémarre au point où il était arrêté. resume() doit être appelé par un thread extérieur à celui suspendu, et dans ce cas il y a une classe séparée appelé Resumer qui fait juste ça. Chacune des classes démontrant suspend/resume a un resumer associé :

 
Sélectionnez
///:Continuing
/////////// Bloqué via suspend() ///////////
class SuspendResume extends Blockable {
  public SuspendResume(Container c) {
    super(c);    
    new Resumer(this); 
  }
}

class SuspendResume1 extends SuspendResume {
  public SuspendResume1(Container c) { super(c);}
  public synchronized void run() {
    while(true) {
      i++;
      update();
      suspend(); // Déprecié en Java 1.2
    }
  }
}

class SuspendResume2 extends SuspendResume {
  public SuspendResume2(Container c) { super(c);}
  public void run() {
    while(true) {
      change();
      suspend(); // Déprecié en Java 1.2
    }
  }
  public synchronized void change() {
      i++;
      update();
  }
}

class Resumer extends Thread {
  private SuspendResume sr;
  public Resumer(SuspendResume sr) {
    this.sr = sr;
    start();
  }
  public void run() {
    while(true) {
       try {
        sleep(1000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
      sr.resume(); // Déprecié en Java 1.2
    }
  }
} ///:Continued

SuspendResume1 a aussi une méthode synchronized run(). Une nouvelle fois, lorsque vous démarrerez ce thread vous verrez que son Peeker associé se bloque en attendant que le verrou devienne disponible, ce qui n'arrive jamais. Ce problème est corrigé comme précédemment dans SuspendResume2, qui ne place pas la méthode run() entièrement synchronize mais utilise une méthode synchronized change() séparée.

Vous devez être au courant du fait que Java 2 déprécie l'utilisation de suspend() et resume(), parce que suspend() prend le verrou et est sujet aux deadlock. En fait, vous pouvez facilement avoir un certain nombre d'objets verrouillés s'attendant les uns les autres, et cela entraînera le gel du programme. Alors que vous pouvez voir leurs utilisations dans des programmes plus vieux, vous ne devriez pas utiliser suspend() et resume(). La solution adéquate est décrite plus tard dans ce chapitre.

XVI-C-1-c. Attendre et notifier

Dans les deux premiers exemples, il est important de comprendre que ni sleep() ni suspend() ne libèrent le verrou lorsqu'ils sont appelés. Vous devez faire attention à cela en travaillant avec les verrous. D'un autre côté, la méthode wait() libère le verrou quand elle est appelée, ce qui signifie que les autres méthodes synchronized de l'objet thread peuvent être appelées pendant un wait(). Dans les deux classes suivantes, vous verrez que la méthode run() est totalement synchronized dans les deux cas, toutefois, le Peeker a encore un accès complet aux méthodes synchronized pendant un wait(). C'est parce que wait() libère le verrou sur l'objet quand il suspend la méthode dans laquelle il a été appelé.

Vous verrez également qu'il y a deux formes de wait(). La première prend un argument en millisecondes qui a la même signification que dans sleep() : pause pour cette période de temps. La différence est que dans wait(), le verrou de l'objet est libéré et vous pouvez sortir du wait() à cause d'un notify() aussi bien que parce que le temps est écoulé.

La seconde forme ne prend pas d'arguments, et signifie que le wait() continuera jusqu'à ce qu'un notify() arrive et ne sera pas automatiquement terminé après un temps donné.

Le seul point commun entre wait() et notify() est que ce sont toutes les deux des méthodes de la classe de base Object et non une partie de Thread comme le sont sleep(), suspend(), et resume(). Alors que cela parait un peu étrange au début - avoir quelque chose exclusivement pour le threading comme partie de la classe de base universelle - c'est essentiel puisqu'elles manipulent le verrou qui fait aussi partie de chaque objet. Le résultat est que vous pouvez mettre un wait() dans n'importe quelle méthode synchronized, du fait qu'il y a du threading intervenant dans cette classe particulière. En fait, la seule place où vous pouvez appeler wait() ou notify() est dans une méthode ou un bloc synchronized. Si vous appelez wait() ou notify() dans une méthode qui n'est pas synchronized, le programme se compilera, mais quand vous l'exécuterez, vous obtiendrez une IllegalMonitorStateException avec le message peu intuitif « current thread not owner. » Notez que sleep(), suspend(), et resume() peuvent toutes être appelées dans des méthodes non synchronized puisqu'elles ne manipulent pas le verrou.

Vous pouvez appeler wait() ou notify() seulement pour votre propre verrou. De nouveau, vous pouvez compiler un code qui essaye d'utiliser le mauvais verrou, mais il se produira le même message IllegalMonitorStateException que précédemment. Vous ne pouvez pas tricher avec le verrou de quelqu'un d'autre, mais vous pouvez demander à un autre objet de réaliser une opération qui manipule son verrou. Une approche possible est de créer une méthode synchronized qui appelle notify() pour son propre objet. Toutefois, dans Notifier vous verrez que notify() est appelé dans un bloc synchronized :

 
Sélectionnez
synchronized(wn2) {
  wn2.notify();
}

wn2 est l'objet de type WaitNotify2. Cette méthode, qui ne fait pas partie de WaitNotify2, acquiert le verrou sur l'objet wn2, à ce point il lui est possible d'appeler notify() pour wn2 et vous n'obtiendrez pas d'IllegalMonitorStateException.

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 contrôle les changements. Ainsi, on dispose d'un moyen de synchronisation entre les threads.

XVI-C-1-d. 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 aux 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'autres données.

 
Sélectionnez
///: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.

XVI-C-1-e. 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 tous les objets Peeker, qui donnent une démonstration 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 "a 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).

 
Sélectionnez
///: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, chacun crée et démarre automatiquement son propre Peeker. Donc vous verrez les Peekers tourner avant que les threads Blockable ne démarrent. C'est important, puisque certains des Peekers seront bloqués et stopperont quand les threads Blockable démarreront, et c'est essentiel de voir et de comprendre cet aspect particulier du blocage.

XVI-C-2. 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 interblocages.

XVI-C-2-a. 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'interblocage 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 inconsistant (« damaged ») les autres threads peuvent les voir et les modifier 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 :

 
Sélectionnez
//: 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 interblocages. Quand vous appelez suspend(), le thread cible s'arrête, mais il conserve les verrous qu'il a acquis à ce point. Ainsi aucun autre thread 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 interblocage. Vous ne devriez pas utiliser suspend() et resume(), mais plutôt mettre un drapeau dans votre classe Thread pour indiquer 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 :

 
Sélectionnez
//: c14:Suspend.java
// L'approche alternative à l'utilisation de suspend()
// et resume(), qui sont dépréciés 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 réveille wait() dans une clause synchronized la méthode fauxResume() doit aussi être synchronized afin qu'elle acquière 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émentée ; c'est comme un suspend() qui ne peut pas être réactivé, donc elle a les mêmes problèmes d'interblocage 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'interblocage 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'exceptionnelle conception et l'infaillibilité claironnée 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 dirigent le show - ils découvrent un problème et ils le fixent. Je trouve cela beaucoup plus prometteur 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 stagner.

La priorité d'un thread dit à l'ordonnanceur [scheduler] l'importance de ce thread. S'il y a un certain nombre de threads bloqués et en attente d'exécution, l'ordonnanceur exécutera celui avec la plus haute priorité en premier. Cependant, cela ne signifie pas que les threads avec des priorités plus faibles ne tourneront pas (en fait, vous ne pouvez pas avoir d'interblocage à cause des priorités). Les threads de priorités plus faibles ont juste tendance à tourner moins souvent.

Bien qu'il soit intéressant de connaître et de jouer avec les priorités, en pratique vous n'avez pratiquement jamais besoin de gérer les priorités par vous-même. Donc, soyez libre de passer le reste de cette session si les priorités ne vous intéressent pas.

XVI-C-3. Lire et changer les priorités

Vous pouvez lire la priorité d'un thread avec getPriority() et la changer avec setPriority(). La forme des précédents exemples « counter » peut être utilisée pour montrer l'effet des changements de priorités. Dans cette applet vous verrez que les compteurs ralentissent quand les threads associés ont leur priorité diminuée :

 
Sélectionnez
//: c14:Counter5.java
// Ajuster les priorités des threads.
// <applet code=Counter5 width=450 height=600>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

class Ticker2 extends Thread {
  private JButton
    b = new JButton("Toggle"),
    incPriority = new JButton("up"),
    decPriority = new JButton("down");
  private JTextField
    t = new JTextField(10),
    pr = new JTextField(3); // Affiche la priorité
  private int count = 0;
  private boolean runFlag = true;
  public Ticker2(Container c) {
    b.addActionListener(new ToggleL());
    incPriority.addActionListener(new UpL());
    decPriority.addActionListener(new DownL());
    JPanel p = new JPanel();
    p.add(t);
    p.add(pr);
    p.add(b);
    p.add(incPriority);
    p.add(decPriority);
    c.add(p);
  }
  class ToggleL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      runFlag = !runFlag;
    }
  }
  class UpL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int newPriority = getPriority() + 1;
      if(newPriority > Thread.MAX_PRIORITY)
        newPriority = Thread.MAX_PRIORITY;
      setPriority(newPriority);
    }
  }
  class DownL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int newPriority = getPriority() - 1;
      if(newPriority < Thread.MIN_PRIORITY)
        newPriority = Thread.MIN_PRIORITY;
      setPriority(newPriority);
    }
  }
  public void run() {
    while (true) {
      if(runFlag) {
        t.setText(Integer.toString(count++));
        pr.setText(
          Integer.toString(getPriority()));
      }
      yield();
    }
  }
}

public class Counter5 extends JApplet {
  private JButton
    start = new JButton("Start"),
    upMax = new JButton("#004488">"Inc Max Priority"),
    downMax = new JButton("#004488">"Dec Max Priority");
  private boolean started = false;
  private static final int SIZE = 10;
  private Ticker2[] s = new Ticker2[SIZE];
  private JTextField mp = new JTextField(3);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    for(int i = 0; i < s.length; i++)
      s[i] = new Ticker2(cp);
    cp.add(new JLabel(
      "MAX_PRIORITY = " + Thread.MAX_PRIORITY));
    cp.add(new JLabel("MIN_PRIORITY = "
      + Thread.MIN_PRIORITY));
    cp.add(new JLabel("#004488">"Group Max Priority = "));
    cp.add(mp);
    cp.add(start);
    cp.add(upMax);
    cp.add(downMax);
    start.addActionListener(new StartL());
    upMax.addActionListener(new UpMaxL());
    downMax.addActionListener(new DownMaxL());
    showMaxPriority();
    // Affiche récursivement les groupes de thread parents:
    ThreadGroup parent =
      s[0].getThreadGroup().getParent();
    while(parent != null) {
      cp.add(new Label(
        "Parent threadgroup max priority = "
        + parent.getMaxPriority()));
      parent = parent.getParent();
    }
  }
  public void showMaxPriority() {
    mp.setText(Integer.toString(
      s[0].getThreadGroup().getMaxPriority()));
  }
  class StartL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      if(!started) {
        started = true;
        for(int i = 0; i < s.length; i++)
          s[i].start();
      }
    }
  }
  class UpMaxL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int maxp =
        s[0].getThreadGroup().getMaxPriority();
      if(++maxp > Thread.MAX_PRIORITY)
        maxp = Thread.MAX_PRIORITY;
      s[0].getThreadGroup().setMaxPriority(maxp);
      showMaxPriority();
    }
  }
  class DownMaxL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      int maxp =
        s[0].getThreadGroup().getMaxPriority();
      if(--maxp < Thread.MIN_PRIORITY)
        maxp = Thread.MIN_PRIORITY;
      s[0].getThreadGroup().setMaxPriority(maxp);
      showMaxPriority();
    }
  }
  public static void main(String[] args) {
    Console.run(new Counter5(), 450, 600);
  }
} ///:~

Ticker2 suit la forme établie plus tôt dans ce chapitre, mais il y a un JTextField supplémentaire pour afficher la priorité du thread et deux boutons de plus pour incrémenter et décrémenter la priorité.

Vous pouvez également noter l'utilisation de yield(), qui rend volontairement la main à l'ordonnanceur. Sans cela le mécanisme de multithreading fonctionne encore, mais vous remarquerez qu'il tourne plus lentement (essayez de supprimer l'appel à yield() pour le voir). Vous pouvez aussi appeler sleep(), mais alors la vitesse de comptage sera contrôlée par la durée du sleep() au lieu de la priorité.

Le init() dans Counter5 crée un tableau de dix Ticker2 ; leurs boutons et champs sont placés sur le formulaire par le constructeur de Ticker2. Counter5 ajoute des boutons pour tout démarrer aussi bien que pour incrémenter et décrémenter la priorité maximum du groupe de thread. En plus, il y a des labels qui affichent les priorités maximum et minimum possibles pour un thread et un JTextField pour montrer la priorité maximum du groupe de thread. (La prochaine section décrira les groupes de threads.) Finalement, les priorités des groupes de threads parents sont aussi affichées comme labels.

Quand vous pressez un bouton « up » ou « down », la priorité du Tricker2 est rapportée et incrémentée ou décrémentée en conséquence.

Quand vous exécutez ce programme, vous noterez plusieurs choses. Tout d'abord, la priorité du groupe de threads est par défaut cinq. Même si vous décrémentez la priorité maximum en dessous de cinq avant de démarrer les threads (ou avant de les créer, ce qui nécessite un changement du code), chaque thread aura une priorité par défaut de cinq.

Le test simple est de prendre un compteur et de décrémenter sa priorité jusqu'à un, et observez qu'il compte beaucoup plus lentement. Mais maintenant, essayez de l'incrémenter de nouveau. Vous pouvez le ramener à la priorité du groupe de thread, mais pas plus haut. Maintenant, décrémentez la priorité du groupe de threads. Les priorités des threads restent inchangées, mais si vous essayez de les modifier dans un sens ou dans l'autre vous verrez qu'elles sauteront automatiquement à la priorité du groupe de thread. Les nouveaux threads auront également une priorité par défaut qui peut être plus haute que la priorité du groupe. (Ainsi la priorité du groupe n'est pas un moyen d'empêcher les nouveaux threads d'avoir des priorités plus hautes que celles existantes.)

Finalement, essayez d'incrémenter la priorité maximum du groupe. On ne peut pas le faire. Vous pouvez seulement réduire la priorité maximum d'un groupe de thread, pas l'augmenter.

XVI-C-4. Les groupes de threads

Tous les threads appartiennent à un groupe de thread. Ce peut être soit le groupe de threads par défaut, soit un groupe de threads que vous spécifiez explicitement quand vous créez le thread. À la création, le thread est attaché à un groupe et ne peut pas en changer. Chaque application a au moins un thread qui appartient au groupe de threads système. Si vous créez plus de threads sans spécifier de groupe, ils appartiendront aussi au groupe de threads système.

Les groupes de threads doivent aussi appartenir à d'autres groupes de threads. Le groupe de threads auquel un nouveau appartient doit être spécifié dans le constructeur. Si vous créez un groupe de threads sans spécifier de groupe auquel le rattacher, il sera placé dans le groupe de threads système. Ainsi, tous les groupes de threads de votre application auront en fin de compte le groupe de threads système comme parent.

La raison de l'existence des groupes de threads est difficile à déterminer à partir de la littérature, qui tend à être confuse sur le sujet. Il est souvent cité des raisons de sécurités. D'après Arnold & Gosling, « Les threads d'un groupe peuvent modifier les autres threads du groupe, y compris ceux situés plus bas dans la hiérarchie. Un thread ne peut pas modifier les threads extérieurs à son propre groupe ou les groupes qu'il contient. » Il est difficile de savoir ce que « modifier » est supposé signifier ici. L'exemple suivant montre un thread dans un sous-groupe « feuille » modifier les priorités de tous les threads de son arbre de groupe de threads aussi bien qu'appeler une méthode pour tous les threads de son arbre.

 
Sélectionnez
//: c14:TestAccess.java
// Comment les threads peuvent accéder aux 
// autres threads dans un groupe de threads parent.

public class TestAccess {
  public static void main(String[] args) {
    ThreadGroup 
      x = new ThreadGroup("x"),
      y = new ThreadGroup(x, "y"),
      z = new ThreadGroup(y, "z");
    Thread
      one = new TestThread1(x, "one"),
      two = new TestThread2(z, "two");
  }
}

class TestThread1 extends Thread {
  private int i;
  TestThread1(ThreadGroup g, String name) {
    super(g, name);
  }
  void f() {
    i++; // modifie ce thread
    System.out.println(getName() + " f()");
  }
}

class TestThread2 extends TestThread1 {
  TestThread2(ThreadGroup g, String name) {
    super(g, name);
    start();
  }
  public void run() {
    ThreadGroup g =
      getThreadGroup().getParent().getParent();
    g.list();
    Thread[] gAll = new Thread[g.activeCount()];
    g.enumerate(gAll);
    for(int i = 0; i < gAll.length; i++) {
      gAll[i].setPriority(Thread.MIN_PRIORITY);
      ((TestThread1)gAll[i]).f();
    }
    g.list();
  }
} ///:~

Dans main(), plusieurs ThreadGroups sont créés, placés en feuilles des autres : x n'a pas d'autres arguments que son nom (une String), ainsi il est automatiquement placé dans le groupe de threads « système », tandis que y est sous x et z est sous y. Notez que l'initialisation se passe dans l'ordre textuel donc ce code est légal.

Deux threads sont créés et placés dans différents groupes de threads. TestThread1 n'a pas de méthode run(), mais a une méthode f() qui modifie le thread et écrit quelque chose afin que vous puissiez voir qu'elle a été appelée. TestThread2 est une sous-classe de TestThread1 et sa méthode run() est assez élaborée. Elle récupère d'abord le groupe de threads du thread courant, puis remonte deux niveaux dans l'arbre d'héritage en utilisant getParent(). (Ceci est étudié puisque j'ai intentionnellement placé l'objet TestThread2 deux niveaux plus bas dans la hiérarchie.) À ce point, un tableau de références sur Thread est créé en utilisant la méthode activeCount() pour demander combien de threads sont dans ce groupe de threads et tous les groupes fils. La méthode enumerate() place les références à tous ces threads dans le tableau gAll, ensuite je me déplace simplement dans la totalité du tableau en appelant la méthode f() pour chaque thread, ainsi qu'en modifiant la priorité. Ainsi, un thread dans un groupe de threads « feuille » modifie les threads dans les groupes de threads parents.

La méthode de débogage list() affiche toute l'information sur un groupe de threads sur la sortie standard ce qui aide beaucoup lorsqu'on examine le comportement d'un groupe de threads. Voici la sortie du programme :

 
Sélectionnez
java.lang.ThreadGroup[name=x,maxpri=10]
    Thread[one,5,x]
    java.lang.ThreadGroup[name=y,maxpri=10]
        java.lang.ThreadGroup[name=z,maxpri=10]
            Thread[two,5,z]
one f()
two f()
java.lang.ThreadGroup[name=x,maxpri=10]
    Thread[one,1,x]
    java.lang.ThreadGroup[name=y,maxpri=10]
        java.lang.ThreadGroup[name=z,maxpri=10]
            Thread[two,1,z]

Non seulement list() affiche le nom du ThreadGroup ou du Thread, mais elle affiche aussi le nom du groupe de threads et sa priorité maximum. Pour les threads, le nom de thread est également affiché, suivi par la priorité du thread et le groupe auquel il appartient. Notez que list() indente les threads et groupes de threads pour indiquer qu'ils sont enfants du groupe de threads non indenté.

Vous pouvez voir que f() est appelé par la méthode run() de TestThread2, il est donc évident que tous les threads d'un groupe sont vulnérables. Cependant, vous ne pouvez accéder seulement aux threads embranchés sur votre propre arbre du groupe de threads système, et peut-être que c'est ce que signifie « sûr. » Vous ne pouvez pas accéder à l'arbre du groupe de threads système d'un autre.

XVI-C-4-a. Contrôler les groupes de threads

En dehors de l'aspect sécurité, une chose pour laquelle les groupes de threads semblent être utiles est le contrôle : vous pouvez effectuer certaines opérations sur un groupe de threads entier avec une seule commande. L'exemple suivant le démontre, et les restrictions sur les priorités avec les groupes de threads. Les numéros entre parenthèses donnent une référence pour comparer la sortie.

 
Sélectionnez
//: c14:ThreadGroup1.java
// Comment les groupes de threads contrôlent les priorités
// des threads qui les composent.

public class ThreadGroup1 {
  public static void main(String[] args) {
    // Récupère le thread system & imprime ses Info:
    ThreadGroup sys = 
      Thread.currentThread().getThreadGroup();
    sys.list(); // (1)
    // Réduit la priorité du groupe de threads système:
    sys.setMaxPriority(Thread.MAX_PRIORITY - 1);
    // Augmente la priorité du thread principal:
    Thread curr = Thread.currentThread();
    curr.setPriority(curr.getPriority() + 1);
    sys.list(); // (2)
    // Essaie de créer un nouveau groupe avec une priorité maximum:
    ThreadGroup g1 = new ThreadGroup("#004488">"g1");
    g1.setMaxPriority(Thread.MAX_PRIORITY);
    // Essaie de créer un nouveau thread avec une priorité maximum:
    Thread t = new Thread(g1, "A");
    t.setPriority(Thread.MAX_PRIORITY);
    g1.list(); // (3)
    // Reduit la priorité maximum de g1, puis essaie
    // de l'augmenter:
    g1.setMaxPriority(Thread.MAX_PRIORITY - 2);
    g1.setMaxPriority(Thread.MAX_PRIORITY);
    g1.list(); // (4)
    // Essaie de créer un nouveau thread avec une priorité maximum:
    t = new Thread(g1, "B");
    t.setPriority(Thread.MAX_PRIORITY);
    g1.list(); // (5)
    // Diminue la priorité maximum en dessous 
    // de la priorité par défaut du thread:
    g1.setMaxPriority(Thread.MIN_PRIORITY + 2);
    // Regarde la priorité d'un nouveau thread
    // avant et après son changement:
    t = new Thread(g1, "C");
    g1.list(); // (6)
    t.setPriority(t.getPriority() -1);
    g1.list(); // (7)
    // Fait de g2 un groupe de threads fils de g1 et
    // essaie d'augmenter sa priorité:
    ThreadGroup g2 = new ThreadGroup(g1, "#004488">"g2");
    g2.list(); // (8)
    g2.setMaxPriority(Thread.MAX_PRIORITY);
    g2.list(); // (9)
    // Ajoute un banc de nouveaux threads à g2:
    for (int i = 0; i < 5; i++)
      new Thread(g2, Integer.toString(i));
    // Montre des informations sur tous les groupes de threads
    // et threads:
    sys.list(); // (10)
    System.out.println("Starting all threads:");
    Thread[] all = new Thread[sys.activeCount()];
    sys.enumerate(all);
    for(int i = 0; i < all.length; i++)
      if(!all[i].isAlive())
        all[i].start();
    // Suspend & Arrête tous les threads de  
    // ce groupe et de ses sous-groupes:
    System.out.println("All threads started");
    sys.suspend(); // Deprecié en Java 2
    // On n'arrive jamais ici...
    System.out.println("All threads suspended");
    sys.stop(); //  Deprecié en Java 2
    System.out.println("All threads stopped");
  }
} ///:~
 
Sélectionnez
(1) ThreadGroup[name=system,maxpri=10]
      Thread[main,5,system]
(2) ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]
(3) ThreadGroup[name=g1,maxpri=9]
      Thread[A,9,g1]
(4) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]
(5) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]
      Thread[B,8,g1]
(6) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,6,g1]
(7) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,3,g1]
(8) ThreadGroup[name=g2,maxpri=3]
(9) ThreadGroup[name=g2,maxpri=3]
(10)ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]
      ThreadGroup[name=g1,maxpri=3]
        Thread[A,9,g1]
        Thread[B,8,g1]
        Thread[C,3,g1]
        ThreadGroup[name=g2,maxpri=3]
          Thread[0,6,g2]
          Thread[1,6,g2]
          Thread[2,6,g2]
          Thread[3,6,g2]
          Thread[4,6,g2]
Starting all threads:
All threads started

Tous les programmes ont au moins un thread qui tourne, et la première action de main() est d'appeler la méthode static de Thread nommée currentThread(). Depuis ce thread, le groupe de threads est produit et list() est appelé sur le résultat. La sortie est :

 
Sélectionnez
(1) ThreadGroup[name=system,maxpri=10]
      Thread[main,5,system]

Vous pouvez voir que le nom du groupe de threads principal est system, et le nom du thread principal est main, et il appartient au groupe de threads system.

Le second exercice montre que la priorité maximum du groupe system peut être réduite et le thread main peut avoir sa priorité augmentée.

 
Sélectionnez
(2) ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]

Le troisième exercice crée un nouveau groupe de threads, g1, qui appartient automatiquement au groupe de threads system puisqu'il n'est rien spécifié d'autre. Un nouveau thread A est placé dans g1. Après avoir essayé de positionner la priorité maximum de ce groupe au plus haut niveau possible et la priorité de A au niveau le plus élevé, le résultat est :

 
Sélectionnez
(3) ThreadGroup[name=g1,maxpri=9]
      Thread[A,9,g1]

Ainsi, il n'est pas possible de changer la priorité maximum d'un groupe de threads au-delà de celle de son groupe de threads parent.

Le quatrième exercice réduit la priorité maximum de g1 de deux et essaie ensuite de l'augmenter jusqu'à Thread.MAX_PRIORITY. Le résultat est :

 
Sélectionnez
(4) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]

Vous pouvez voir que l'augmentation à la priorité maximum ne fonctionne pas. Vous pouvez seulement diminuer la priorité maximum d'un groupe de threads, pas l'augmenter. Notez également que la priorité du thread A n'a pas changé, et est maintenant plus grande que la priorité maximum du groupe de threads. Changer la priorité maximum d'un groupe de threads n'affecte pas les threads existants.

Le cinquième exercice essaie de créer un nouveau thread avec une priorité au maximum :

 
Sélectionnez
(5) ThreadGroup[name=g1,maxpri=8]
      Thread[A,9,g1]
      Thread[B,8,g1]

Le nouveau thread ne peut pas être changé à une priorité plus haute que la priorité maximum du groupe de threads.

La priorité par défaut du thread pour ce programme est six ; c'est la priorité avec laquelle un nouveau thread sera créé et à laquelle il restera si vous ne manipulez pas la priorité. L'exercice 6 diminue la priorité maximum du groupe de threads en dessous de la priorité par défaut pour voir ce qui se passe quand vous créez un nouveau thread dans ces conditions :

 
Sélectionnez
(6) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,6,g1]

Étant donné que la priorité maximum du groupe de threads est trois, le nouveau thread est encore créé en utilisant la priorité par défaut de six. Ainsi, la priorité maximum du groupe de threads n'affecte pas la priorité par défaut. (En fait, il ne semble pas y avoir de moyen pour positionner la priorité par défaut des nouveaux threads.)

Après avoir changé la priorité, en essayant de la décrémenter par pas de un, le résultat est :

 
Sélectionnez
(7) ThreadGroup[name=g1,maxpri=3]
      Thread[A,9,g1]
      Thread[B,8,g1]
      Thread[C,3,g1]

La priorité maximum du groupe de threads est forcée seulement lorsque vous essayez de changer la priorité du thread.

Une expérience similaire est effectuée en (8) et (9), dans lequel un nouveau groupe de threads g2 est créé comme un fils de g1 et sa priorité maximum est changée. Vous pouvez voir qu'il n'est pas impossible pour la priorité maximum de g2 de devenir plus grande que celle de g1 :

 
Sélectionnez
(8) ThreadGroup[name=g2,maxpri=3]
(9) ThreadGroup[name=g2,maxpri=3]

Notez également que g2 est automatiquement mise à la priorité maximum du groupe de threads g1 dès la création de g2.

Après toutes ces expériences, le système de groupes de threads est entièrement donné :

 
Sélectionnez
(10)ThreadGroup[name=system,maxpri=9]
      Thread[main,6,system]
      ThreadGroup[name=g1,maxpri=3]
        Thread[A,9,g1]
        Thread[B,8,g1]
        Thread[C,3,g1]
        ThreadGroup[name=g2,maxpri=3]
          Thread[0,6,g2]
          Thread[1,6,g2]
          Thread[2,6,g2]
          Thread[3,6,g2]
          Thread[4,6,g2]

Donc à cause des règles sur les groupes de threads, un groupe fils doit toujours avoir une priorité maximum plus petite ou égale à celle de son parent.

La dernière partie de ce programme démontre les méthodes pour un groupe de threads entier. Dans un premier temps le programme parcourt l'intégralité de l'arbre de threads et démarre chaque thread qui ne l'est pas déjà. Par drame, le groupe system est alors suspendu et finalement arrêté. (Bien qu'il soit intéressant de voir que suspend() et stop() fonctionnent sur un groupe de threads entier, vous devez garder en tête que ces méthodes sont dépréciées en Java 2.) Mais quand vous suspendez le groupe system vous suspendez aussi le thread main et le programme entier s'arrête, donc il ne quittera jamais le point où les threads sont stoppés. Actuellement, si vous stoppez le thread main il déclenche une exception ThreadDeath, ce n'est donc pas une chose à faire. Puisque ThreadGroup hérite de Object, qui contient la méthode wait(), vous pouvez aussi choisir de suspendre le programme pour un certain nombre de secondes en appelant wait(secondes * 1000). Ce qui doit acquérir le verrou dans un bloc synchronised, bien sûr.

La classe ThreadGroup a aussi des méthodes suspend() et resume() donc vous pouvez arrêter et démarrer un groupe de threads entier et toutes ces threads et sous-groupes avec une seule commande. (Encore une fois, suspend() et resume() sont dépréciés en Java 2.)

Les groupes de threads peuvent paraître un peu mystérieux au premier abord, mais garder en mémoire que vous ne les utiliserez probablement directement très peu souvent.

XVI-D. Runnable revisité

Précédemment dans ce chapitre, j'ai suggéré que vous deviez faire très attention avant de faire d'une applet ou une Frame principale une implémentation de Runnable. Bien sûr, si vous devez hériter d'une classe et que vous voulez ajouter un comportement multitâche à la classe, Runnable est la solution correcte. L'exemple final de ce chapitre exploite ceci en créant une classe Runnable JPanel qui se peint de différentes couleurs. Cette application prend des valeurs depuis la ligne de commande pour déterminer la taille de la grille de couleurs et la durée du sleep() entre les changements de couleur. En jouant sur ces valeurs, vous découvrirez des possibilités intéressantes et parfois inexplicables des threads :

 
Sélectionnez
//: c14:ColorBoxes.java
// Utilisation de l'interface Runnable.
// <applet code=ColorBoxes width=500 height=400>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import com.bruceeckel.swing.*;

class CBox extends JPanel implements Runnable {
  private Thread t;
  private int pause;
  private static final Color[] colors = { 
    Color.black, Color.blue, Color.cyan, 
    Color.darkGray, Color.gray, Color.green,
    Color.lightGray, Color.magenta, 
    Color.orange, Color.pink, Color.red, 
    Color.white, Color.yellow 
  };
  private Color cColor = newColor();
  private static final Color newColor() {
    return colors[
      (int)(Math.random() * colors.length)
    ];
  }
  public void paintComponent(Graphics  g) {
    super.paintComponent(g);
    g.setColor(cColor);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
  }
  public CBox(int pause) {
    this.pause = pause;
    t = new Thread(this);
    t.start(); 
  }
  public void run() {
    while(true) {
      cColor = newColor();
      repaint();
      try {
        t.sleep(pause);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    } 
  }
} 

public class ColorBoxes extends JApplet {
  private boolean isApplet = true;
  private int grid = 12;
  private int pause = 50;
  public void init() {
    // Récupère les paramètres depuis la page web:
    if (isApplet) {
      String gsize = getParameter("grid");
      if(gsize != null)
        grid = Integer.parseInt(gsize);
      String pse = getParameter("pause");
      if(pse != null)
        pause = Integer.parseInt(pse);
    }
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(grid, grid));
    for (int i = 0; i < grid * grid; i++)
      cp.add(new CBox(pause));
  }
  public static void main(String[] args) {
    ColorBoxes applet = new ColorBoxes();
    applet.isApplet = false;
    if(args.length > 0)
      applet.grid = Integer.parseInt(args[0]);
    if(args.length > 1) 
      applet.pause = Integer.parseInt(args[1]);
    Console.run(applet, 500, 400);
  }
} ///:~

ColorBoxes est l'applet/application habituelle avec une méthode init() qui créé la GUI. Elle positionne la GridLayout afin d'avoir une grille de cellules dans chaque dimension. Ensuite elle ajoute le nombre approprié d'objet CBox pour remplir la grille, passant la valeur pause à chacune. Dans main() vous pouvez voir comment pause et grid ont des valeurs par défaut qui peuvent être changées si vous passez des arguments à la ligne de commande, ou en utilisant des arguments de l'applet.

CBox est là où tout le travail s'effectue. Elle hérite de JPanel et implémente l'interface Runnable ainsi chaque JPanel peut aussi être un Thread. Souvenez-vous que quand vous implémentez Runnable, vous ne faites pas un objet Thread, mais juste une classe qui a une méthode run(). Ainsi, vous devez créer explicitement un objet Thread et passer l'objet Runnable au constructeur, puis appelez start() (ce qui est fait dans le constructeur). Dans CBox ce thread est appelé t.

Remarquez le tableau colors, qui est une énumération de toutes les couleurs de la classe Color. Il est utilisé dans newColor() pour produire une couleur sélectionnée au hasard. La couleur de la cellule courante est cColor.

paintComponent() est assez simple - elle place juste la couleur à cColor et remplit intégralement le JPanel avec cette couleur.

Dans run(), vous voyez la boucle infinie qui place la cColor à une nouvelle couleur prise au hasard et ensuite appelle repaint() pour la montrer. Puis le thread passe dans sleep() pour le temps spécifié sur la ligne de commande.

C'est précisément parce que le design est flexible et que le threading est attaché à chaque élément JPanel, que vous pouvez expérimenter en créant autant de threads que vous voulez. (En réalité, il y a une restriction imposée par le nombre de threads que votre JVM peut confortablement gérer.)

Ce programme fait aussi un benchmark intéressant, puisqu'il peut montrer des différences de performance dramatiques entre deux implémentations du threading dans les JVM.

XVI-D-1. Trop de threads

À un certain point, vous trouverez que ColorBoxes ralentit. Sur ma machine, cela se passe quelque part après une grille 10 x 10. Pourquoi est-ce que cela arrive ? Vous soupçonnez naturellement que Swing ait quelque chose à voir avec ça, donc voici un exemple qui teste cette prémisse en faisant moins de threads. Le code suivant est réorganisé afin qu'une ArrayList implements Runnable et cette ArrayList gère un nombre de blocs de couleurs et en choisit une au hasard pour la mise à jour. Puis un certain nombre de ces objets ArrayList sont créés, en fonction d'une approximation de la dimension de la grille que vous choisissez. Au résultat, vous avez beaucoup moins de threads que de blocs de couleurs, donc s'il y a une accélération nous saurons que c'était à cause du trop grand nombre de threads dans l'exemple précédent :

 
Sélectionnez
//: c14:ColorBoxes2.java
// Compromis dans l'utilisation de threads.
// <applet code=ColorBoxes2 width=600 height=500>
// <param name=grid value="12">
// <param name=pause value="50">
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.bruceeckel.swing.*;

class CBox2 extends JPanel {
  private static final Color[] colors = { 
    Color.black, Color.blue, Color.cyan, 
    Color.darkGray, Color.gray, Color.green,
    Color.lightGray, Color.magenta, 
    Color.orange, Color.pink, Color.red, 
    Color.white, Color.yellow 
  };
  private Color cColor = newColor();
  private static final Color newColor() {
    return colors[
      (int)(Math.random() * colors.length)
    ];
  }
  void nextColor() {
    cColor = newColor();
    repaint();
  }
  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(cColor);
    Dimension s = getSize();
    g.fillRect(0, 0, s.width, s.height);
  }
}

class CBoxList 
  extends ArrayList implements Runnable {
  private Thread t;
  private int pause;
  public CBoxList(int pause) {
    this.pause = pause;
    t = new Thread(this);
  }
  public void go() { t.start(); }
  public void run() {
    while(true) {
      int i = (int)(Math.random() * size());
      ((CBox2)get(i)).nextColor();
      try {
        t.sleep(pause);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    } 
  }
  public Object last() { return get(size() - 1);}
}

public class ColorBoxes2 extends JApplet {
  private boolean isApplet = true;
  private int grid = 12;
  // Pause par défaut plus courte que dans ColorBoxes:
  private int pause = 50;
  private CBoxList[] v;
  public void init() {
    // Récupère les paramètres de la page Web:
    if (isApplet) {
      String gsize = getParameter("grid");
      if(gsize != null)
        grid = Integer.parseInt(gsize);
      String pse = getParameter("pause");
      if(pse != null)
        pause = Integer.parseInt(pse);
    }
    Container cp = getContentPane();
    cp.setLayout(new GridLayout(grid, grid));
    v = new CBoxList[grid];
    for(int i = 0; i < grid; i++)
      v[i] = new CBoxList(pause);
    for (int i = 0; i < grid * grid; i++) {
      v[i % grid].add(new CBox2());
      cp.add((CBox2)v[i % grid].last());
    }
    for(int i = 0; i < grid; i++)
      v[i].go();
  }   
  public static void main(String[] args) {
    ColorBoxes2 applet = new ColorBoxes2();
    applet.isApplet = false;
    if(args.length > 0)
      applet.grid = Integer.parseInt(args[0]);
    if(args.length > 1) 
      applet.pause = Integer.parseInt(args[1]);
    Console.run(applet, 500, 400);
  }
} ///:~

CBox2 est similaire à CBox : elle se peint avec une couleur choisie au hasard. Mais c'est tout ce qu'une CBox2 fait. Tout le threading a été déplacé dans CBoxList.

Le CBoxList pourrait aussi avoir hérité de Thread et avoir un objet membre de type ArrayList. Ce design a l'avantage que les méthodes add() et get() puissent avoir des arguments et un type de valeur de retour spécifiques au lieu de ceux de la classe générique Object. (Leurs noms pourraient aussi être changés en quelque chose de plus court.) Cependant, le design utilisé ici semble au premier coup d'œil nécessiter moins de code. En plus, on garde automatiquement tous les autres comportements d'un ArrayList. Avec tous les cast et parenthèses nécessaires pour get(), ça ne devrait pas être le cas dès que votre code central augmente.

Comme précédemment, quand vous implémentez Runnable vous ne disposez pas de tout l'équipement que vous avez avec Thread, donc vous devez créer un nouveau Thread et passer vous-même à son constructeur pour avoir quelque chose à démarrer (par start()), comme vous pouvez le voir dans le constructeur de CBoxList et dans go(). La méthode run() choisit simplement un numéro d'élément au hasard dans la liste et appelle nextColor() pour cet élément ce qui provoque la sélection au hasard d'une nouvelle couleur.

En exécutant ce programme, vous voyez qu'effectivement il tourne et répond plus vite (par exemple, quand vous l'interrompez, il s'arrête plus rapidement), et il ne semble pas ralentir autant pour de grandes tailles de grilles. Ainsi, un nouveau facteur est ajouté à l'équation du threading : vous devez regarder pour voir si vous n'avez pas « trop de threads » (quelle qu'en soit la signification pour votre programme et plate-forme en particulier, le ralentissement dans ColorBoxes apparaît comme provenant du fait qu'il n'y a qu'un thread qui répond pour toutes les colorations, et qu'il est ralenti par trop de requêtes). Si vous avez trop de threads, vous devez essayer d'utiliser des techniques comme celle ci-dessus pour « équilibrer » le nombre de threads dans votre programme. Si vous voyez des problèmes de performances dans un programme multithread vous avez maintenant plusieurs solutions à examiner :

  1. Avez-vous assez d'appels à sleep(), yield(), et/ou wait() ? ;
  2. Les appels à sleep() sont-ils assez longs ? ;
  3. Faites-vous tourner trop de threads ? ;
  4. Avez-vous essayé différentes plates-formes et JVM ?

Des questions comme celles-là sont la raison pour laquelle la programmation multithread est souvent considérée comme un art.

XVI-E. Résumé

Il est vital d'apprendre quand utiliser le multithreading et quand l'éviter. La principale raison pour l'utiliser est pour gérer un nombre de tâches qui mélangées rendront plus efficace l'utilisation de l'ordinateur (y compris la possibilité de distribuer de façon transparente les tâches sur plusieurs CPU) ou être plus pratique pour l'utilisateur. L'exemple classique de répartition de ressources est l'utilisation du CPU pendant les attentes d'entrées/sorties. L'exemple classique du côté pratique pour l'utilisateur est la surveillance d'un bouton « stop » pendant un long téléchargement.

Les principaux inconvénients du multithreading sont :

  1. Ralentissement en attente de ressources partagées ;
  2. Du temps CPU supplémentaire nécessaire pour gérer les threads ;
  3. Complexité infructueuse, comme l'idée folle d'avoir un thread séparé pour mettre à jour chaque élément d'un tableau ;
  4. Pathologies incluant starving [=mourir de faim!?], racing, et interblocage [deadlock].

Un avantage supplémentaire des threads est qu'ils substituent des contextes d'exécution « légers » (de l'ordre de 100 instructions) aux contextes d'exécutions lourds des processus (de l'ordre de 1000 instructions). Comme tous les threads d'un processus donné partagent le même espace mémoire, un contexte léger ne change que l'exécution du programme et les variables locales. D'un autre côté, un changement de processus - le changement de contexte lourd -  doit échanger l'intégralité de l'espace mémoire.

Le threading c'est comme marcher dans un monde entièrement nouveau et apprendre un nouveau langage de programmation entier, ou au moins un nouveau jeu de concepts de langage. Avec l'apparition du support des threads dans beaucoup de systèmes d'exploitation de micro-ordinateurs, des extensions pour les threads sont aussi apparues dans les langages de programmations ou bibliothèques. Dans tous les cas, la programmation de thread (1) semble mystérieuse et requiert un changement dans votre façon de penser la programmation et (2) apparaît comme similaire au support de thread dans les autres langages, donc quand vous comprenez les threads, vous les comprenez dans une langue commune. Et bien que le support des threads puisse faire apparaître Java comme un langage plus compliqué, n'accuser pas Java. Les threads sont difficiles.

Une des plus grandes difficultés des threads se produit lorsque plus d'un thread partage une ressource - comme la mémoire dans un objet - et que vous devez être sûr que plusieurs threads n'essayent pas de lire et changer cette ressource en même temps. Cela nécessite l'utilisation judicieuse du mot-clef synchronized, qui est un outil bien utile, mais qui doit être bien compris parce qu'il peut introduire silencieusement des situations d'interblocage.

En plus, il y a un certain art dans la mise en application des threads. Java est conçu pour résoudre vos problèmes - au moins en théorie. (Créer des millions d'objets pour une analyse d'un ensemble fini d'éléments dans l'ingénierie, par exemple, devrait être faisable en Java). Toutefois, il semble qu'il y ait une borne haute au nombre de threads que vous voudrez créer, parce qu'à un certain point un grand nombre de threads semble devenir lourd. Ce point critique n'est pas dans les milliers comme il devrait être avec les objets, mais plutôt à quelques centaines, quelquefois moins que 100. Comme vous créez souvent seulement une poignée de threads pour résoudre un problème, c'est typiquement pas vraiment une limite, bien que dans un design plus général cela devient une contrainte.

Une importante conséquence non évidente du threading est que, en raison de l'ordonnancement des threads, vous pouvez classiquement rendre vos applications plus rapides en insérant des appels à sleep() dans la boucle principale de run(). Cela fait définitivement ressembler ça à un art, en particulier quand la longueur du délai semble augmenter les performances. Bien sûr, la raison pour laquelle cela arrive est que des délais plus courts peuvent causer la fin de sleep() de l'interruption du scheduler avant que le thread tournant soit prêt à passer au sleep, forçant le scheduler à l'arrêter et le redémarrer plus tard afin qu'il puisse finir ce qu'il était en train de faire puis de passer à sleep. Cela nécessite une réflexion supplémentaire pour réaliser quel désordre règne dans tout ça.

Une chose que vous devez noter comme manquant dans ce chapitre est un exemple d'animation, ce qui est une des choses les plus populaires faites avec des applets. Toutefois, une solution complète (avec du son) à ce problème est disponible avec le Java JDK (disponible sur java.sun.com) dans la section démo. En plus, nous pouvons nous attendre à un meilleur support d'animation comme partie des futures versions de Java, tandis que des solutions très différentes du Java, non programmables, pour les animations apparaissent sur le Web qui seront probablement supérieures aux approches traditionnelles. Pour des explications à propos du fonctionnement des animations Java, voyez Core Java 2 par Horstmann & Cornell, Prentice-Hall, 1997. Pour des discussions plus avancées sur le threading, voyez Concurrent Programming in Java de Doug Lea, Addison-Wesley, 1997, ou Java Threads de Oaks & Wong, O&rsquo;Reilly, 1997.

XVI-F. Exercices

Les solutions des exercices sélectionnés peuvent être trouvées dan le document électronique The Thinking in Java Annotated Solution Guide, disponible pour une petite contribution sur www.BruceEckel.com.

  1. Faites hériter une classe de Thread et redéfinissez la méthode run(). Dans run(), affichez un message, puis appelez sleep(). Répétez cela trois fois, puis sortez de run(). Placer un message de démarrage dans le constructeur et redéfinissez finalize() pour afficher un message d'arrêt. Faites une classe thread séparée qui appelle System.gc() et System.runFinalization() dans run(), affichant également un message. Faites plusieurs objets threads des deux types et exécutez-les pour voir ce qui se passe.
  2. Modifiez Sharing2.java pour ajouter un bloc synchronized dans la méthode run() de TwoCounter à la place de la synchronisation sur l'intégralité de la méthode run().
  3. Créez deux sous-classes de Thread une avec un run() qui démarre, capture la référence à un second objet Thread et appelle alors wait(). Le run() de l'autre classe devrait appeler notifyAll() pour le premier thread après qu'un certain nombre de secondes se soient écoulées, ainsi le premier thread peut afficher un message.
  4. Dans Ticker2 de Counter5.java, supprimez le yield() et expliquez les résultats. Remplacez le yield() avec un sleep() et expliquez les résultats.
  5. Dans ThreadGroup1.java, remplacez l'appel à sys.suspend() par un appel à wait() pour le bon groupe de threads, entraînant une attente de deux secondes. Pour que ça marche correctement vous devez acquérir le verrou pour sys dans un bloc synchronized.
  6. Changez Daemons.java pour que main() ait un sleep() à la place d'un readLine(). Expérimentez avec différents temps pour le sleep pour voir se qui se passe.
  7. Dans le titre X, localisez l'exemple GreenhouseControls.java, qui est constitué de trois fichiers. Dans Event.java, la classe Event est basée sur l'observation du temps. Changez Event pour qu'il soit un Thread, et changez le reste du design pour qu'il fonctionne avec le nouvel Event basé sur Thread.
  8. Modifiez l'exercice 7 pour que la classe java.util.Timer trouvée dans le JDK 1.3 soit utilisée pour faire tourner le système.
  9. En commençant par SineWave.java du titre XV, créez un programme (une applet/application utilisant la classe Console) qui dessine un signal sinus animé qui apparaît en défilant sur la fenêtre comme un oscilloscope, dirigeant l'animation avec un Thread. La vitesse de l'animation pourra être contrôlée avec un contrôle java.swing.JSlider.
  10. Modifiez l'exercice 9 afin que de multiples signaux sinus puissent être contrôlés par des tags HTML ou des paramètres de la ligne de commande.
  11. Modifiez l'exercice 9 afin que la classe java.swing.Timer soit utilisée pour diriger l'animation. Notez la différence entre celle-ci et java.util.Timer.

précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.