Les Threads

Chapitres traités   

Vous connaissez probablement déjà le multitâche : la possibilité d'avoir plusieurs programmes travaillant en même temps. Par exemple, il est possible avec un système multitâche d'imprimer un document en même temps que vous en modifiez un autre ou que vous téléchargez vos e-mails. Aujourd'hui, vous disposez probablement d'un ordinateur avec plusieurs coeurs dans le même processeur, mais le nombre d'actions s'exécutant simultanément peut être beaucoup plus conséquent et ne se limite pas au nombre de coeurs effectifs.

Le système d'exploitation affecte une tranche de temps à chaque action, ce qui donne l'illusion d'une activité parallèle. Cette répartition des ressources est possible parce que la plupart des programmes ne se servent pas de l'intégralité du temps machine. Par exemple, lorsqu'un utilisateur saisit des données rapidement, il ne prend qu'un vingtième de seconde par caractère.

Choix du chapitre Processus et multitâche

Les programmes utilisant plusieurs threads développent l'idée du multitâche en l'implémentant à un niveau plus bas : des programmes individuels peuvent effectuer plusieurs tâches en même temps. Chaque tâche est traditionnellement appelée un thread. Les programmes qui peuvent exécuter plusieurs threads en même temps sont appelés des programmes à multithreads.

Chaque thread peut être considéré comme possédant un contexte indépendant (un contexte est composé d'un processeur, d'un ensemble de registres, de mémoire et d'un code particulier).

Quelle est donc la différence entre plusieurs processus et plusieurs threads ? La différence essentielle est qu'un processus possède une copie unique de ses propres variables, alors que les threads partagent les mêmes données. Cela peut paraître risqué, et cela l'est parfois. Mais il est bien plus rapide de créer et de détruire des threads individuels que de créer un nouveau processus. C'est pourquoi tous les systèmes d'exploitation modernes supportent les multithreads. De plus, la communication entre les processus est bien plus lente et plus restrictive qu'entre des threads.

Les multithreads sont très utiles dans la pratique. Par exemple, un navigateur doit pouvoir gérer plusieurs hôtes, ouvrir une fenêtre de courrier électronique, afficher une nouvelle page et télécharger des données en même temps. Le langage de programmation java lui-même se sert d'un thread pour gérer le ramasse-miettes en tâche de fond, ce qui vous épargne de gérer la mémoire vous-même ! Les programmes ayant une interface utilisateur graphique possèdent un thread séparé pour récupérer les événements de l'interface utilisateur.

Choix du chapitre Qu'est-ce qu'un thread ?

Commençons par étudier un programme qui ne se sert pas de plusieurs threads et qui, par conséquent, empêche l'utilisateur d'effectuer plusieurs tâches avec ce programme. Ce programme fait rebondir une balle en la déplaçant en permanence. Si la balle arrive contre le mur, il la fait rebondir.



Codage correspondant
package threads;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.ArrayList;
import javax.swing.*;

public class SimpleBalle extends JFrame {
   private JButton démarrer = new JButton("Démarrer");
   private JButton fermer = new JButton("Fermer");
   private JPanel boutons = new JPanel();
   private Panneau panneau = new Panneau();

   public SimpleBalle() {
      super("Rebond d'une balle avec un seul Thread");
      panneau.setBackground(Color.ORANGE);
      add(panneau);
      add(boutons, BorderLayout.SOUTH);
      boutons.add(démarrer);
      boutons.add(fermer);
      démarrer.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            ajoutBalle();
         }
      });
      fermer.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            System.exit(0);
         }
      });
      setSize(500, 400);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private void ajoutBalle() {
      try {
         Balle balle = new Balle();
         panneau.ajout(balle);
         for (int i=0; i<1000; i++) {
            balle.déplace(panneau.getBounds());
            panneau.paintComponent(panneau.getGraphics());
            Thread.sleep(5);
         }
      }
      catch (InterruptedException ex) { }
   }

   private class Panneau extends JPanel {
      private ArrayList<Balle> balles = new ArrayList<Balle>();

      public void ajout(Balle balle) {
         balles.add(balle);
      }

      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         for (Balle balle : balles) surface.fill(balle.getForme());
      }
   }

   private class Balle {
      private double x, y, dx=5, dy=5;

      public void déplace(Rectangle2D zone) {
         x+=dx;
         y+=dy;
         if (x < zone.getMinX()) { x = zone.getMinX();  dx = -dx; }
         if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15;  dx = -dx; }
         if (y < zone.getMinY()) { y = zone.getMinY();  dy = -dy; }
         if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15;  dy = -dy; }
      }

      public Ellipse2D getForme() {
         return new Ellipse2D.Double(x, y, 15, 15);
      }
   }

   public static void main(String[] args) { new SimpleBalle(); }
}

Dès que vous cliquez sur le bouton "Démarrer", le programme lance la balle à partir du coin supérieur gauche de l'écran et celle-ci commence à rebondir. Le gestionnaire du bouton "Démarrer" appelle la méthode ajoutBalle(). Cette méthode contient une boucle progressant sur 1000 déplacements. Chaque appel à la méthode déplace() déplace légèrement la balle, ajuste la direction si elle rebondit sur un mur, puis redessine l'écran.

La méthode statique sleep() de la classe Thread effectue une pause du nombre de millisecondes donné.
§

L'appel à Thread.sleep() ne crée pas un nouveau thread, sleep() est une méthode statique de la classe Thread qui met le thread courant en sommeil (5 ms). Bien sûr, vous avez la possiblité de changer la durée du sommeil pour obtenir l'apparence d'un déplacement plus ou moins rapide. La méthode sleep() peut déclencher une exception InterruptedException, il faut donc utiliser un bloc try-catch si nous désirons la capturer.

Nous reviendrons sur cette exception et sur son gestionnaire un peu plus tard. Pour l'instant, nous terminons simplement le rebond si l'exception survient.
§

Si vous exécutez ce programme, vous verrez que la balle rebondit très bien, mais qu'elle bloque complètement l'application. Si ces rebonds vous lassent et que vous ne vouliez pas attendre les 1000 déplacements, il suffit normalement de cliquer sur le bouton "Fermer". Le problème, c'est que les boutons disparaissent. Du coup, la balle continue quand même son trajet. Il est ainsi impossible d'interagir avec le programme tant que la balle rebondit.

Si vous étudiez attentivement le code, vous remarquez l'appel panneau.paintComponent(panneau.getGraphics()); dans la méthode ajoutBalle() de la classe SimpleThread. Cela est assez étrange, vous devriez normalement appeler la méthode repaint() et laisser le soin à AWT d'obtenir le contexte graphique et de redessiner l'écran. Toutefois, si vous essayez de réaliser cet appel dans ce programme, vous découvrirez que l'écran n'est jamais redessiné puisque la méthode ajoutBalle() a totalement pris le pas sur le traitement.

Remarquez également que le composant de balle étend JPanel, ce qui simplifie l'effacement de l'arrière-plan. Dans le programme suivant, nous utiliserons un autre thread pour calculer la position de la balle et nous emploierons à nouveau la méthode repaint() de JComponent.

A l'évidence, ce n'est pas une situation acceptable et nous ne souhaitons pas que les programmes utilisés se comportent de cette manière lorsqu'ils doivent effectuer des tâches plutôt longues. Après tout, lorsque vous lisez des données en passant par une connexion réseau, il est trop courant d'être bloqué par une tâche gourmande en temps machine, que vous souhaiteriez vraiment interrompre.

Par exemple, supposons que vous chargiez une grande image et que vous décidiez, après avoir vue une partie, que vous ne voulez pas voir le reste. Il peut alors être très intéressant de pouvoir cliquer sur un bouton "Arrêter" ou "Précédent" pour interrompre le chargement. Dans le prochain chapitre, nous montrerons comment laisser cette possiblité à l'utilisateur en exécutant les parties critiques d'un programme dans un thread séparé.

 

Choix du chapitre Utiliser des threads pour laisser une chance aux autres tâches

Nous allons rendre notre programme de balles plus attentif à l'utilisateur en exécutant le code qui déplace la balle dans un thread séparé. Vous pourrez en fait lancer plusieurs balles. Chacune est déplacée dans son propre thread. En outre, le thread de distribution d'événements AWT continue à fonctionner en parallèle, il s'occupe des événements de l'interface utilisateur.

Chaque thread ayant une occasion de s'exécuter, le thread de répartition d'événements peut savoir si l'utilisateur clique sur le bouton "Fermer" alors que les balles rebondissent. Il peut alors traiter l'action correspondante.

Comme la plupart des ordinateurs ne possèdent pas plusieurs processeurs, la machine virtuelle java se sert d'un mécanisme dans lequel chaque thread a une chance d'être exécuté pendant un bref moment, avant qu'un autre thread soit activé. La machine virtuelle se fonde en général sur le système d'exploitation hôte pour la gestion des threads, et à priorité égale, les threads se partagent à part égale le temps d'exécution :

Nous utilisons l'exemple de la balle rebondissante pour bien demontrer visuellement le besoin de simultanéité. En règle général, inquiétez-vous des calculs long. Votre calcul fera sûrement partie d'un cadre plus général, par exemple l'interface graphique ou le Web. Dès que le cadre appelle l'une de vos méthodes, on s'attend généralement à un retour rapide. Pour toutes les tâches plutôt longues, utilisez systématiquement un thread séparé.

Dans notre prochain programme, nous nous servirons de deux threads. Le premier s'occupe de la balle, et le second (le thread principal) s'occupe de l'interface utilisateur. Comme chaque thread a une chance d'être exécuté, le thread principal en a une de voir que vous cliquez sur le bouton "Fermer", même si la balle continue de rebondir. Il peut alors traiter l'action de fermeture.

Procédure à suivre pour mettre en oeuvre un thread séparé

Voici une procédure très simple pour exécuter du code dans un thread séparé :

  1. Placez le code dans la méthode run() d'une classe qui implémente l'interface Runnable. Cette interface est très simple, elle ne possède que la méthode run() :

    L'interface java.lang.Runnable
    void run()
    Cette méthode doit être redéfinie et vous devez y ajouter les instructions qui doivent être exécutées dans le thread correspondant.
    Vous implémentez simplement une classe comme ceci :
    class MaClasse implements Runnable {
       public void run() {
          // code de la tâche à réaliser
       }
    }
  2. Construisez un objet de votre classe :
    Runnable interface =  new MaClasse();
  3. Construisez un objet de la classe Thread à partir de l'interface Runnable :
    Thread tâche =  new Thread(interface);
  4. Démarrer le thread :
    tâche.start();

Mise en oeuvre sur les balles rebondissantes

Pour exécuter notre programme dans un thread séparé, nous devons simplement implémenter une classe BalleSéparée et placer le code de l'animation dans la méthode run(), comme dans le code suivant :

private class BalleSéparée implements Runnable {
   private Balle balle;
   public BalleSéparée(Balle balle) {
       this.balle = balle;
   }
      
   public void run() {
      try {
         for (int i=0; i<1000; i++) {
            balle.déplace(panneau.getBounds());
            panneau.repaint();
            Thread.sleep(5);
         }
      }
      catch (InterruptedException ex) { }
   }
}

Une fois de plus, nous devons intercepter une exception appelée InterruptedException que la méthode sleep() menace de déclencher. Nous découvrirons cette exception dans le prochain chapitre. Typiquement, l'interruption sert à demander la fin d'un thread. De même, notre méthode run() se termine lorsqu'une InterruptedException se présente.

Dès que l'utilisateur clique sur le bouton "Démarrer", la méthode ajoutBalle() lance un nouveau thread :
private void ajoutBalle() {
   Balle balle = new Balle();      
   panneau.ajout(balle);
   new Thread(new BalleSéparée(balle)).start();
}

C'est tout ce qu'il faut savoir ! vous savez maintenant lancer des tâches en parallèles. Le reste de cette étude vous montrera comment contrôler l'interaction entre les threads.

Codage correspondant
package threads;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.ArrayList;
import javax.swing.*;

public class PlusieursBalles extends JFrame {
   private JButton démarrer = new JButton("Démarrer");
   private JButton fermer = new JButton("Fermer");
   private JPanel boutons = new JPanel();
   private Panneau panneau = new Panneau();

   public PlusieursBalles() {
      super("Rebond de plusieurs balles avec plusieurs threads séparés");
      panneau.setBackground(Color.ORANGE);
      add(panneau);
      add(boutons, BorderLayout.SOUTH);
      boutons.add(démarrer);
      boutons.add(fermer);
      démarrer.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            ajoutBalle();
         }
      });
      fermer.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            System.exit(0);
         }
      });
      setSize(500, 400);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private void ajoutBalle() {
      Balle balle = new Balle();      
      panneau.ajout(balle);
      new Thread(new BalleSéparée(balle)).start();
   }

   private class BalleSéparée implements Runnable {
      private Balle balle;

      public BalleSéparée(Balle balle) {
          this.balle = balle;
      }
      
      public void run() {
         try {
            for (int i=0; i<1000; i++) {
               balle.déplace(panneau.getBounds());
               panneau.repaint();
               Thread.sleep(5);
            }
         }
         catch (InterruptedException ex) { }
      }
   }

   private class Panneau extends JPanel {
      private ArrayList<Balle> balles = new ArrayList<Balle>();

      public void ajout(Balle balle) {
         balles.add(balle);
      }

      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         for (Balle balle : balles) surface.fill(balle.getForme());
      }
   }

   private class Balle {
      private double x, y, dx=5, dy=5;

      public void déplace(Rectangle2D zone) {
         x+=dx;
         y+=dy;
         if (x < zone.getMinX()) { x = zone.getMinX();  dx = -dx; }
         if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15;  dx = -dx; }
         if (y < zone.getMinY()) { y = zone.getMinY();  dy = -dy; }
         if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15;  dy = -dy; }
      }

      public Ellipse2D getForme() {
         return new Ellipse2D.Double(x, y, 15, 15);
      }
   }

   public static void main(String[] args) { new SimpleBalle(); }
}
Vous pouvez aussi définir un thread en formant une sous-classe de la classe Thread, comme ceci :
class MaClasse extends Thread {
   public void run() {
      // code de la tâche à réaliser
   }
}

Vous construisez ensuite un objet de la sous-classe et appelez sa méthode start(). Toutefois, cette approche n'est plus conseillée. Il vaut mieux découpler la tâche qui doit être exécutée en parallèle de son mécanisme d'exécution. Si vous disposez de plusieurs tâches, créer un thread séparé pour chacune serait trop onéreux. Vous pouvez plutôt utiliser un pool de threads (voir le chapitre Executors, plus loin dans cette étude).

ATTENTION : N'appelez pas la méthode run() de la classe Thread ou de l'objet Runnable car cela exécute directement la tâche sur le même thread. Aucun nouveau thread n'est démarré. Appelez plutôt la méthode start(). Elle créera un nouveau thread qui exécutera la méthode run() quand cela lui sera permis.

 

La classe java.lang.Thread
Thread()
Construit un nouveau thread. Vous devez activer le thread avec la méthode start() qui s'occupe d'activer la méthode run() lorsque le temps imparti lui est dédié.
Thread(Runnable cible)
Construit un nouveau thread qui appelle la méthode run() de la cible spécifiée.
static void sleep(long millisecondes)
Place le thread en cours d'exécution en état de veille, pendant tout le temps spécifié en millisecondes.
void start()
Lance ce thread et appelle sa méthode run(). Cette méthode revient immédiatement. Le nouveau thread est exécuté en "même temps".
void run()
Appelle la méthode run() de l'interface Runnable associée.

 

Choix du chapitre Interrompre des threads

Un thread s'arrête naturellement lorsque sa méthode run() se termine, en exécutant une instruction return après la dernière instruction du corps de la méthode ou si une exception survient qui ne soit pas interceptée dans la méthode.

Dans la dernière version de Java, il existait également une méthode stop() qu'un autre thread aurait pu appeler pour terminer un thread. Mais cette méthode est aujourd'hui obsolète (voir plus loin dans ce chapitre).

Il existe une méthode pour obliger un thread à se terminer. Il est possible effectivement d'utiliser la méthode interrupt() pour exiger la terminaison d'un thread.

Lorsque la méthode interrupt() est appelée sur un thread, l'indication du thread est définie sur interrompu (interrupted). Il s'agit d'un indicateur booléen présent dans chaque thread. Chaque thread doit, à l'occasion, vérifier s'il a été interrompu.

Pour savoir si l'indication a été définie sur interrompu, appelez la méthode currentThread() de la classe Thread pour obtenir le thread actuel, puis appelez la méthode isInterrupted() :

while (!Thread.currentThread().isInterrupted() && encore du tavail) {
   // faire le travail
}

Attention, un thread ne fonctionne pas en permanence. Il doit se rendormir ou attendre régulièrement, pour donner aux autres threads une chance d'être exécutés. Du coup, lorsqu'un thread dort, il ne peut pas déterminer s'il est interrompu. Lorsque la méthode interrupt() est appelée sur un thread actuellement bloqué, l'appel bloquant (comme sleep() ou wait()) est terminé par une InterruptedException.

Il n'existe aucune spécification demandant qu'un thread doit être terminé. L'interruption d'un thread se contente d'attirer son attention. Le thread interrompu peut choisir comment réagir devant l'interruption. Certains threads sont tellement importants qu'ils peuvent tout simplement ignorer leur interruption en continuant leur travail. Mais, plus couramment, un thread essaiera d'interpréter l'interruption comme une demande d'arrêt.

La méthode run() de ce type de thread a la forme suivante :
public void run() {
   try {
      ...
      while (!Thread.currentThread().isInterrupted() && encore du tavail) {
         // faire le travail
      }
   }
   catch(InterruptedException ex) {
      // le thread a été interrompu pendant sleep() ou wait()
   }
   finally {
      // nettoyage, si nécessaire
   }
   // sort de la méthode run() et met fin au thread
}

La vérification isInterrupted() n'est ni nécessaire ni utile si vous appelez la méthode sleep() (ou une autre méthode pouvant être interrompue) après chaque itération. Si vous appelez la méthode sleep() lorsque l'indication est définie sur interrompu, elle ne se met pas en veille. Elle efface le status (!) et lance une exception InterruptedException.

Ainsi, si votre boucle appelle sleep(), ne vous inquiétez pas de vérifier l'interruption et interceptez simplement l'exception, comme ceci :

public void run() {
   try {
      ...
      while (encore du tavail) {
         // faire le travail
         Thread.sleep(délai);
      }
   }
   catch(InterruptedException ex) {
      // le thread a été interrompu pendant le sommeil
   }
   finally {
      // nettoyage, si nécessaire
   }
   // sort de la méthode run() et met fin au thread
}

Curieusement, il existe deux méthodes très similaires, interrupted() et isInterrupted(). La méthode interrupted() est une méthode statique qui vérifie si le thread actuel a été interrompu. De plus, un appel à la méthode interrupted() réinitialise l'indication "interrompu" d'un thread. D'un autre côté, la méthode isInterrupted() est une méthode d'instance que vous pouvez utiliser pour vérifier que n'importe quel thread a été interrompu. L'indication "interrompu" de son attribut n'est pas modifié.

ATTENTION : Pour l'instant, notre clause catch est vierge. Ne le laissez pas comme cela ! Si vous ne trouvez rien à faire dans la clause catch, il reste un choix résonnable qui consiste à prévenir que le thread courant vient de s'arrêter inopinément au moyen de la commande Thread.currentThread().interrupt().

La classe java.lang.Thread
void interrupt()
Envoie une demande d'interruption à un thread. L'indication "interrompu" du thread est mise à true. Si le thread est actuellement bloqué par un appel à sleep() ou à wait(), une InterruptedException est déclenchée.
static boolean interrupted()
Regarde si le thread actuel (c'est à dire, le thread qui exécute cette instruction) a été interrompu. Notez qu'il s'agit d'une méthode statique. Cet appel possède un effet annexe : il met l'indication "interrompu" du thread courant à false.
boolean isInterrupted()
Regarde si un thread a été interrompu. Contrairement à la méthode statique interrupted(), cet appel ne change pas l'indication "interrompu" du thread.
static Thread currentThread()
Renvoie l'objet Thread représentant le thread en cours d'exécution.
boolean isAlive()
Renvoie true si le thread est démarré et n'est pas encore terminé.

Exemple d'application des interruptions de thread

Nous allons mettre en oeuvre toute cette technique d'interruption à l'aide du programme précédant. Toutefois, il n'existera qu'une seule balle dans l'application qui rebondit indéfiniment sauf avis contraire.

ATTENTION : Cette fois-ci, le thread séparé est le panneau lui-même. La classe BalleSéparée n'existe plus. La balle est donc déclarée à l'intérieur du panneau est existe indéfiniment jusqu'à l'arrêt complet du programme. Grâce à cette démarche, lorsque que nous demandons de relancer la balle, elle se déplace de nouveau à partir de l'endroit où elle s'était arrêtée.

Comme nouveauté, nous avons également un bouton "Arrêter" qui permet d'interrompre le thread séparé (représenté par le panneau) qui va finir par ne plus exister (le panneau lui-même est toujours présent). Après avoir interrompu le thread, un nouveau thread est créé afin de remplacer l'ancien. Lorsque vous cliquez sur le bouton "Démarrer", si le thread n'est pas encore en mode d'exécution, il va être lancé au moyen de la méthode start().



Codage correspondant
package threads;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.ArrayList;
import javax.swing.*;

public class SimpleBalle extends JFrame {
   private JButton démarrer = new JButton("Démarrer");
   private JButton arrêter = new JButton("Arrêter");
   private JPanel boutons = new JPanel();
   private Panneau panneau = new Panneau();
   private Thread tâcheSéparée = new Thread(panneau);

   public SimpleBalle() {
      super("Rebond d'une balle avec un seul Thread");
      panneau.setBackground(Color.ORANGE);
      add(panneau);
      add(boutons, BorderLayout.SOUTH);
      boutons.add(démarrer);
      boutons.add(arrêter);
      démarrer.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            if (!tâcheSéparée.isAlive()) tâcheSéparée.start();
         }
      });
      arrêter.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            tâcheSéparée.interrupt();
            tâcheSéparée = new Thread(panneau);
         }
      });
      setSize(500, 400);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private class Panneau extends JPanel implements Runnable {
      private Balle balle = new Balle();

      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         surface.fill(balle.getForme());
      }

      public void run() {
         try {
            while (!Thread.currentThread().interrupted()) {
               balle.déplace(getBounds());
               repaint();
               Thread.sleep(4);
            }
         }
         catch (InterruptedException ex) { Thread.currentThread().interrupt(); }
      }
   }

   private class Balle {
      private double x, y, dx=5, dy=5;

      public void déplace(Rectangle2D zone) {
         x+=dx;
         y+=dy;
         if (x < zone.getMinX()) { x = zone.getMinX();  dx = -dx; }
         if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15;  dx = -dx; }
         if (y < zone.getMinY()) { y = zone.getMinY();  dy = -dy; }
         if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15;  dy = -dy; }
      }

      public Ellipse2D getForme() {
         return new Ellipse2D.Double(x, y, 15, 15);
      }
  }

   public static void main(String[] args) { new SimpleBalle(); }
}

 

Choix du chapitre Les états d'un thread

Les threads peuvent se trouver dans l'un des six états qui suivent.

Pour connaître l'état actuel, appelez simplement la méthode getState().
§

  1. Nouveau : Lorsque vous créez un thread avec l'opérateur new, par exemple new Thread(panneau), le thread n'est pas encore exécuté. Cela signifie qu'il se trouve dans l'état Nouveau. Lorsqu'un thread se trouve dans cet état, le programme n'a pas encore commencé à exécuter le code qui se trouve dans le thread. Il faut faire encore quelques opérations avant que ce thread puisse être exécuté. Ces opérations et l'allocation des ressources nécessaires reviennent à la méthode start().
  2. Exécutable : Une fois que vous avez invoqué la méthode start(), le thread prend l'état Exécutable. Il se peut que le thread exécutable ne soit pas encore en cours d'exécution. C'est au système d'exploitation de fournir au thread une fenêtre d'exécution. Lorsque le code à l'intérieur d'un thread commence à être exécuté, ce thread est en cours d'exécution. Un thread en cours d'exécution reste dans l'état Exécutable.

    Lorsqu'un thread s'exécute, il ne poursuit pas obligatoirement l'opération dans le temps. En réalité, cela peut se révéler souhaitable si des threads sont mis en pause de temps à autre afin que d'autres threads aient une occasion de s'exécuter.

    Ce mécanisme dépend directement du système d'exploitation. La plupart des systèmes accordent à chaque thread une tranche de temps pour effectuer sa tâche. Lorsque cette tranche de temps est écoulée, le système d'exploitation passe a un autre thread.

    Lorsque vous sélectionnez le thread suivant, le système d'exploitation prend en compte les priorités du thread. Voir plus loin pour en savoir plus sur les priorités.

    Sur une machine multiprocesseur, chaque processeur peut exécuter un thread et plusieurs méthodes threads peuvent s'exécuter réellement en parallèle. Bien entendu, s'il y a plus de threads que de processeurs, le gestionnaire est toujours responsable du découpage en tranches.

    N'oubliez jamais qu'un thread exécutable peut être ou non en cours d'exécution.
    §

  3. Bloqué ou En attente : Lorsqu'un thread est bloqué ou en attente, il est temporairement inactif. Il n'exécute aucun code et consomme un minimum de ressources. C'est au gestionnaire de thread de le réactiver. Les détails dépendent de la manière dont l'état d'inactivité a été atteint.
  4. Lorsqu'un thread est bloqué ou en attente (ou, bien sûr, lorsqu'il se termine), un autre thread est prévu pour être exécuté. Lorsqu'un thread est réactivé (par exemple, parce que sa temporisation a expiré ou qu'il a réussi à acquérir un verrou), le gestionnaire regarde s'il possède une priorité supérieure à celle du thread actuellement en cours d'exécution. Dans ce cas, il interrompt le thread actuel et sélectionne un nouveau thread.


  5. Expiré ou Terminé : Un thread peut se terminer pour l'une des deux raisons suivantes :
  6. En particulier, il est possible de tuier un thread en invoquant la méthode stop(). Cette méthode déclenche une erreur ThreadDeath qui tue le thread. Cependant, la méthode stop() n'est plus utilisée et il faut éviter de l'appeler dans votre code.

    Pour savoir si un thread est couramment actif, c'est à dire qu'il est soit exécutable , soit bloqué, utilisez la méthode isAlive(). Cette méthode renvoie true si le thread est exécutable ou bloqué, et false si le thread est nouveau et pas encore exécutable, ou si le thread est mort.

La classe java.lang.Thread
void join()
Attend que le thread spécifié se termine.
void join(long millisecondes)
Attend que le thread spécifié meure ou que le nombre spécifié de millisecondes s'écoule.
Thread.State getState()
Récupère l'état de ce thread ; peut être l'une des constantes NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING ou TERMINATED.
void stop()
Arrête le thread. Cette méthode est aujourd'hui obsolète.
void suspend()
Suspend l'exécution du thread. Cette méthode est aujourd'hui obsolète.
void resume()
Reprend le thread. Cette méthode n'est valide qu'après un appel à suspend(). Cette méthode est aujourd'hui obsolète.
boolean isAlive()
Renvoie true si le thread est démarré et n'est pas encore terminé.

Choix du chapitre Propriétés d'un thread

Dans les sections suivantes, nous verrons les diverses propriétés des threads :

  1. Priorités des threads,
  2. Thread demon,
  3. Gestionnaires d'exceptions non récupérées,
  4. Groupes de threads.

Priorités d'un thread

Dans le langage de programmation Java, chaque thread possède une priorité. Par défaut, un thread hérite de la priorité du thread qui l'a construit. Vous pouvez augmenter ou baisser la priorité de n'importe quel thread avec la méthode setPriority(). La priorité de n'importe quel thread peut être choisi entre :

  1. MIN_PRIORITY : correspondant à 1 dans la classe Thread.
  2. MAX_PRIORITY : correspondant à 10.
  3. NORM_PRIORITY : vaut 5.

Lorsque que le gestionnaire de thread peut choisir un nouveau thread, il choisit généralement le thread de la plus haute priorité. Toutefois, les priorités de threads sont fortement dépendantes du systtème. Lorsque la machine virtuelle compte sur l'implémentation des threads de la plate-forme hôte, les priorités de threads Java sont mis en correspondance avec les niveaux de priorité de la plate-forme hôte, qui peut en avoir plus ou moins.

A titre d'exemple, Windows affiche sept niveaux de priorité. Certaines propritétés Java concorderont avec le même niveau du système d'exploitation. Dans la machine virtuelle de Sun pour Linux, les priorités de threads sont totalement ignorées : tous les threads ont systématiquement la même priorité.

Les programmeurs débutants font parfois trop appel aux priorités de thread. Il existe peu de raisons de tirer un trait sur les priorités, mais ne structurez jamais vos programmes en fonction du niveau de priorité.

ATTENTION : Si vous souhaitez tout de même utiliser les priorités, soyez averti d'une erreur commune aux débutants. Si vous diposez de plusieurs threads à haute priorité qui ne reviennent pas inactifs, ceus de moindre priorité risquent de ne jamais s'exécuter. Lorsque le gestionnaire décide d'exécuter un nouveau thread, il choisira d'abord parmi ceux de plus haute priorité, même si cela peut totalemnt annilhiler les threads de mondre priorité.

 

La classe java.lang.Thread
void setPriority(int priorité)
Définit la priorité de ce thread. Cette priorité doit être comprise entre Thread.MIN_PRIORITY et Thread.MAX_PRIORITY. Utiliser Thread.NORM_PRIORITY pour une priorité normale.
static int MIN_PRIORITY
Définit la priorité minimale qu'un thread peut avoir. La valeur de la priorité minimale est 1.
static int NORM_PRIORITY
Définit la priorité par défaut d'un thread. La valeur de la priorité par défaut est 5.
static int MAX_PRIORITY
Définit la priorité maximale qu'un thread peut avoir. La valeur de la priorité maximale est 10.
static void yield()
Cette méthode arrête le thread en cours d'exécution. S'il existe d'autres threads exécutables dont la priorité est au moins égale à celle de ce thread, ils seront traités par la suite. Notez qu'il s'agit d'une méthode statique.

Thread démons

Un thread peut être transformé en thread démon en appelant la méthode setDaemon() :

thread.setDeamon(true);

Il n'y a rien de démoniaque là-dedans ! un démon est simplement un thread qui n'a aucun autre but dans la vie que de servir d'autres threads. Nous pouvons citer comme exemples des threads chronomètres qui envoient des "pulsations de temps". régulières à d'autres threads ou à des threads qui nettoient les entrées de cache. Lorsqu'il ne reste plus que les threads démons, la machine virtuelle se ferme. Il n'y a en effet aucun intérêt à continuer d'exécuter un programme si tous les threads restants sont des démons.

Les threads démons sont parfois employés par erreur par les débutants qui ne réfléchissent pas aux actions de fermeture. Cela peut toutefois être dangereux. Un thread démon ne doit jamais accéder à une ressource persistante comme un fichier ou une base de données puisqu'il peut se terminer à tout moment, même au milieu d'une opération.

 

La classe java.lang.Thread
void setDaemon(boolean activer)
Signale ce thread comme un démon ou un thread utilisateur. Cette méthode doit être appelée avant le démarrage du thread.

Gestionnaire d'exceptions non récupérées

La méthode run() d'un thread ne peut pas déclencher d'exceptions sous contrôle, mais elle peut être terminée par une exception hors contrôle. Dans ce cas le thread meurt.

Il n'existe toutefois pas de clause catch dans laquelle l'exception peut être propagée. Juste avant que le thread ne meure, l'exception est transférée à un gestionnaire d'exceptions non récupérées.

Ce gestionnaire doit appartenir à une classe qui implémente l'interface Thread.UncaughtExceptionHandler, qui ne possède qu'une seule méthode nommée uncaughtException().

L'interface java.lang.Thread.UncaughtExceptionHandler
void uncaughtException(Thread thread, Throwable erreur)
Définit pour consigner un rapport personnalisé lorsqu'un thread se termine avec une exception non récupérée.
§ thread : le thread terminé du fait d'une exception non récupérée.
§ erreur : l'objet de l'exception non récupérée.

Depuis Java SE 5.0, vous pouvez installer un gestionnaire dans n'importe quel thread avec la méthode setUncaughtExceptionHandler(). Vous pouvez aussi installer un gestionnaire par défaut pour tous les threads avec la méthode statique setDefaultUncaughtExceptionHandler() de la classe Thread. Un gestionnaire de remplacement pourrait utiliser l'API d'identification pour envoyer des rapports sur les exceptions non récupérées à un fichier journal.

Si vous n'installez pas de gestionnaire par défaut, le gestionnaire par défaut vaut null. Mais, si vous n'installez pas de gestionnaire pour un thread particulier, le gestionnaire correspond à l'objet ThreadGroup du thread.

La classe java.lang.Thread
static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler identifiant)
static Thread.UncaughtExceptionHandler getDefaultUncaughtExceptionHandler()
Définissent ou récupèrent le gestionnaire par défaut pour les exceptions non récupérées.
static void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler identifiant)
static Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()
Définissent ou récupèrent le gestionnaire des exceptions non récupérées. Lorsque aucun gestionnaire n'est installé, l'objet du groupe de thread devient le gestionnaire.

 

Groupes de threads

Un groupe de thread est une collection de thread pouvant être gérés ensemble. Par défaut, tous les threads que vous créez appartiennent au même groupe mais vous pouvez créer d'autres groupements.

Depuis que Java SE 5.0 présentent de meilleures fonstionnalités pour les collections de thread, vous ne devez pas utiliser de groupes de thread dans vos propres programmes.

La classe ThreadGroup implémente l'interface Thread.UncaughtExceptionHandler. Sa méthode uncaughtException() réalise l'action suivante :

  1. Si le groupe de threads a un parent, la méthode uncaughtException() du groupe parent est appelée.
  2. Sinon, la méthode Thread.getDefaultUncaughtExceptionHandler() renvoie un gestionnaire non nul, il est appelé.
  3. Sinon, si un Throwable est une instance de ThreadDeath, il ne se passe rien.
  4. Sinon, le nom du thread et la trace de Throwable sont affichés sur System.err.

C'est une trace que vous avez certainement beaucoup vue dans vos programmes.
§

La classe java.lang.ThreadGroup
void uncaughtException(Thread thread, Throwable erreur)
Appelle cette méthode du groupe de thread parent (s'il existe un parent) ou le gestionnaire par défaut de la classe Thread (s'il existe un gestionnaire par défaut). Sinon affiche une trace du flux d'erreur standard.

Toutefois, si erreur est un objet ThreadDeath, la trace est supprimée. Les objets ThreadDeath sont générés par la méthode stop(), aujourd'hui obsolète.
§

 

Choix des chapitres Synchronisation entre threads

Dans la plupart des applications pratiques à base de multithreads, il arrive que plusieurs threads doivent partager un accès aux mêmes objets. Que se passe t-il si deux threads ont accès au même objet et que chacun appelle une méthode qui modifie l'état de cet objet ? comme vous pouvez vous l'imaginer, les threads se font concurrence. Selon l'ordre dans lequel la donnée a été accédée, des objets corrompus peuvent apparaître. Ce type de situation est souvent appelé une condition de course.

Exemple de condition de course

Pour éviter que plusieurs threads ne corrompent des données partagées, vous devez apprendre à synchroniser l'accès. Dans cette section nous allons voir ce qui se passe si nous n'utilisons pas de synchronisation.

Dans le programme de test ci-dessous, nous simulons une banque possédant plusieurs comptes. Nous générons au hasard des transactions qui déplacent de l'argent entre ses comptes. Chaque compte possède un thread. Chaque transaction déplace des quantités aléatoire d'argent, du compte desservi par le thread vers un autre compte choisi aléatoirement :

Codage correspondant
package synchronisation;

public class NonSynchronisé {
   private static final int NOMBRE_COMPTE = 100;
   private static final double SOLDE_INITIAL = 1000.0;

   public static void main(String[] args) { 
      Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL);
      for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start();
   }
}

class Banque {
   private final double[] comptes;

   public Banque(int nombre, double soldeInitiale) {
      comptes = new double[nombre];
      for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale;
   }

   public void transfert(int de, int vers, double somme) {
      if (comptes[de] < somme) return;
      System.out.print(Thread.currentThread());
      comptes[de] -= somme;
      System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
      comptes[vers] += somme;
      System.out.printf(" Balance totale : %10.2f €%n", getBalanceTotale());
   }

   public double getBalanceTotale() {
      double solde = 0.0;
      for (double compte : comptes) solde += compte;
      return solde;
   }

   public int nombreDeComptes() { return comptes.length; }
}

class Transfert implements Runnable {
   private Banque banque;
   private int duCompte;
   private double maximumAutorisé;

   public Transfert(Banque banque, int duCompte, double maximumAutorisé) {
      this.banque = banque;
      this.duCompte = duCompte;
      this.maximumAutorisé = maximumAutorisé;
   }


   public void run() {
      try {
         int versCompte = (int)(banque.nombreDeComptes() * Math.random());
         double montant = maximumAutorisé * Math.random();
         banque.transfert(duCompte, versCompte, montant);
         Thread.sleep((int)(10*Math.random()));
      }
      catch (InterruptedException e) {}
   }
}

Le code de cette simulation est assez simple. Il est principalement constitué :

  1. De la classe Banque et de la méthode tranfert(). Cette méthode transfère une certaine somme d'argent d'un compte vers un autre (nous ne traitons pas les soldes négatifs).
  2. Ainsi que de la classe Transfert. Sa méthode run() sort en permanence de l'argent d'un compte bancaire spécifié. Â chaque itération, la méthode run() choisit au hasard un compte cible et un montant d'argent aléatoire, puis elle appelle la méthode transfert() sur l'objet banque et s'endort.

Lorsque cette simulation est exécutée, nous ne savons pas combien d'argent se trouve dans chaque compte bancaire. En revanche, nous savons que la somme de tous les comptes doit rester constante, puisque nous n'effectuons uniquement que des transferts d'argent entre ces comptes (de la même banque). A la fin de chaque transaction, la méthode transfert() recalcule le total et l'affiche.

run:
...
Thread[Thread-57,5,main] 391,83 € de 57 à 60 Balance totale : 100000,00 € Thread[Thread-59,5,main] 954,27 € de 59 à 3 Balance totale : 100000,00 € Thread[Thread-61,5,main] 255,27 € de 61 à 25 Balance totale : 100000,00 € Thread[Thread-63,5,main] 487,74 € de 63 à 25 Balance totale : 100000,00 € Thread[Thread-65,5,main] 204,55 € de 65 à 91 Balance totale : 100000,00 €
... Thread[Thread-10,5,main] 362,20 € de 10 à 61 Balance totale : 99114,95 € Thread[Thread-12,5,main] 320,41 € de 12 à 45 Balance totale : 99114,95 € Thread[Thread-14,5,main] 873,93 € de 14 à 76 Balance totale : 99114,95 € Thread[Thread-16,5,main] 723,88 € de 16 à 1 Balance totale : 99114,95 € ... BUILD SUCCESSFUL (total time: 0 seconds)

Comme nous pouvons le constater, nous obtenons une erreur importante. Pour certaines transactions, le solde reste inférieur à 100 000 €, ce qui correspond normalement au total correct pour 100 compte de 10 000 € chacun. Mais après un certain moment, le solde total est légèrement modifié.

Lorsque vous excécutez ce programme, vous vous rendez compte que des erreurs se produisent rapidement, ou au contraire qu'il faut un certain temps pour que le solde soit corrompu. Cette situation n'inspire pas confiance et vous n'aurez probablement pas envie de déposer votre argent difficilement gagné dans cette banque.

Explication des conditions de course

Dans la section précédente, nous avons exécuté un programme dans lequel plusieurs threads mettaient à jour des soldes de comptes banquaires. Après un certain temps, des erreurs apparaissaient et une certaine quantité d'argent était soit perdue, soit ajoutée spontanément.

Ce problème se présente lorsque deux threads essaient simultanément de mettre à jour un compte. Supposons que deux threads essaient d'exécuter l'instruction suivante simultanément :

comptes[vers] += somme;

Le problème de cette instruction est qu'elle n'est pas composée d'opérations atomiques. Cette instruction peut en effet être décomposée comme suit :

  1. Charger comptes[vers] dans un registre.
  2. Ajouter somme.
  3. Placer le résultat dans comptes[vers].

Maintenant, supposons que le premier thread exécute les deux premières étapes, puis qu'il soit préempté. Supposons ensuite que le second thread se réveille et qu'il mette à jour la même entrée dans le tableau comptes. Le premier thread se réveille alors et termine sa troisième étape. Cette action annule la modification de l'autre thread. Par conséquent, le total n'est plus correct.

Notre programme de test détecte cette erreur. Naturellement, il existe une légère possibilité que de fausses alarmes se présentent si le thread est interrompu alors qu'il effectue le test !

Probabilité d'erreur

Quelle est la probabilité que cette erreur se produise ? Nous avons déterminé que nous pouvions augmenter la probabilté d'erreur en entrelaçant les instructions d'affichage à celles qui actualisent le solde.

Si vous oubliez les instructions d'affichage, le risque de corruption diminue un peu car chaque thread travaille très peu avant de se rendormir. Il est donc peu probable que le gestionnaire le préempte au milieu du calcul.

Mais le risque de corruption demeure. Si vous exécutez de très nombreux threads sur une machine très chargée, le programme échouera, même lorsque vous aurez éliminé les instruction d'affichage. Il n'existe rien de pire, ou presque, pour un programmeur qu'une erreur qui ne se manifeste que tous les deux ou trois jours.

Le vrai problème réside dans le fait que le travail de la méthode transfert() peut être interrompu en plein milieu. Si nous pouvions nous assurer que la méthode s'exécute jusqu'à la fin, avant la perte de contrôle du thread, l'état du tableau des comptes en banque ne serait jamais corrompu.

Verrous d'objet

Depuis Java SE 5.0, il existe deux mécanismes permettant de protéger un bloc de code d'un accès simultané :

  1. Le langage Java fournit le mot clé synchronised dans ce but : Ce mot clé synchronised fournit automatiquement un verrou ainsi qu'une condition associée, qui simplifie la plupart des cas nécessitant un verrou explicite. Toutefois, nous comprendrons mieux le mot clé synchronised après avoir isolé les verrous et les conditions.

    Le cadre java.util.concurrent propose des classes séparées pour ces mécanismes fondamentaux, que nous expliquerons plus loin dans ce chapitre. Lorsque nous aurons compris ces bases, nous verrons le mot clé synchronised.

  2. Java SE 5.0 a introduit la classe ReentrantLock : Le code principal permettant de protéger un bloc de code avec ReentrantLock est le suivant :
    verrou.lock();   // un objet ReentrantLock
    try {
       // section principale
    }
    finally {
      verrou.unlock();  // vérifier que le verrou est dévérouillé, même si une exception est déclenchée
    }

    Cette construction garantit que seul un thread à la fois puisse entrer dans la section principale. Dès qu'un thread vérouille l'objet verrou, aucun autre thread ne peut entrer dans l'instruction lock(). Lorsque d'autres threads appellent lock(), ils sont bloqués, jusqu'à ce que le premier thread débloque l'objet verrou.

    ATTENTION : Il est particulièrement important que l'opération unlock() soit enserrée dans une clause finally. Si le code de la section critique déclenche une exception, le verrou doit être dévérouillé, faute de quoi les autres threads seront bloqués pour toujours.


    Utilisons un verrou pour protéger la méthode transfert() de la classe Banque
    class Banque {
       private final double[] comptes;
       private Lock verrou = new ReentrantLock();  // ReentrantLock implémente l'interface Lock
    ...
       public void transfert(int de, int vers, double somme) {
          verrou.lock();
          System.out.print(Thread.currentThread());
          comptes[de] -= somme;
          System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
          comptes[vers] += somme;
          System.out.printf("  Balance totale : %10.2f €%n", getBalanceTotale());
          verrou.unlock();
       }
    ...

    Supposons qu'un thread appelle la méthode transfert() et soit préempté avant d'avoir terminé. Supposons ensuite qu'un deuxième thread appelle aussi la méthode transfert(). Le deuxième ne peut pas acquérir le verrou, il est bloqué dans l'appel de la méthode lock(). Il est donc désactivé et doit attendre que le premier thread finisse d'exécuter la méthode tranfert(). Lorsque le premier thread débloque le verrou au travers de la méthode unlock(), le deuxième peut alors continuer.

    Testez-le. Ajoutez le code de vérouillage à la méthode transfert() et exécutez à nouveau le programme. Vous pouvez l'exécuter à l'infini, le solde ne sera jamais corrompu.
    run:
    ...
    Thread[Thread-57,5,main] 391,83 € de 57 à 60 Balance totale : 100000,00 € Thread[Thread-59,5,main] 954,27 € de 59 à 3 Balance totale : 100000,00 € Thread[Thread-61,5,main] 255,27 € de 61 à 25 Balance totale : 100000,00 € Thread[Thread-63,5,main] 487,74 € de 63 à 25 Balance totale : 100000,00 € Thread[Thread-65,5,main] 204,55 € de 65 à 91 Balance totale : 100000,00 €
    ... Thread[Thread-10,5,main] 362,20 € de 10 à 61 Balance totale : 100000,00 € Thread[Thread-12,5,main] 320,41 € de 12 à 45 Balance totale : 100000,00 € Thread[Thread-14,5,main] 873,93 € de 14 à 76 Balance totale : 100000,00 € Thread[Thread-16,5,main] 723,88 € de 16 à 1 Balance totale : 100000,00 € ... BUILD SUCCESSFUL (total time: 0 seconds)
  3. Vous remarquez que chaque objet banque possède son propre objet ReentrantLock. Si deux threads tentent d'accéder au même objet banque, le verrou sert à sérialiser l'accès. Toutefois, si deux threads tentent d'accéder à différents objets banque, chaque thread acquiert un verrou différent et aucun thread n'est bloqué. Ce fonctionnement est souhaitable, car les threads ne doivent pas interférer les uns avec les autres lorsqu'ils manipulent différentes instances de Banque.

    Le verrou est dit rentrant car un thread peut acquérir, à plusieurs reprises, un verrou qu'il possède déjà. Le verrou tient le compte des appels imbriqués à la méthode lock(). Le thread doit appeler unlock() pour chaque appel à lock(), de manière à renoncer au verrou. Du fait de cette caractéristique, le code protégé par un verrou peut appeler une autre méthode qui utilise les mêmes verrous.

    Si, par exemple, la méthode transfert() appelle la méthode getBalanceTotale(), celle-ci vérouille aussi l'objet verrou, lequel affiche maintenant un compte de 2. Lorsque la méthode getBalanceTotale() se termine, le compte revient à 1. Â la fin de la méthode transfert(), le compte est à 0 et le thread renonce au verrou.

    En général, il est souhaitable de protéger les blocs de code qui mettent à jour ou inspectent un objet partagé. Vous êtes alors assuré que ces opérations s'exécutent jusqu'au bout avant qu'un autre thread ne puisse utiliser le même objet.

    Attention : Vous devez prendre garde à ce que le code d'une section principale ne soit pas contourné par le déclanchement d'une exception. Si une exception est déclenchée avant la fin de la section, la clause finally renoncera au verrou, mais l'objet pourra être endommagé.


    L'interface java.util.concurrent.locks.Lock
    void lock()
    Acquiert ce verrou ; se bloque si le verrou appartient à un autre thread.
    void unlock()
    Libère ce bloc.
    La classe java.util.concurrent.locks.ReentrantLock
    ReentrantLock()
    Construit un verrou rentrant pouvant être utilisé pour protéger une section principale.
    ReentrantLock(boolean équitable)
    Construit un verrou avec la règle d'équité donnée. Un verrou juste favorise le thread qui attend depuis plus longtemps. Cette garantie peut toutefois présenter un gros inconvénient au niveau des performances. Ainsi, par défaut, il n'est pas obligatoire que les verrous soient équitables.

    Même si l'équité paraît adaptée, ces verrous sont bien plus lents que les verrous ordinaires. Activez-les seulement si vous voulez véritablement savoir ce que vous faites et si vous avez une bonne raison de les employer. Même si vous utilisez un verrou équitable, vous n'avez aucune garantie que son gestionnaire le soit. S'il choisit de négliger un thread qui attend le verrou depuis longtemps, il n'aura pas l'occasion d'être traité équitablement pas le verrou.


Objets de condition

Très souvent, un thread entre dans une section principale, uniquement pour découvrir qu'il ne peut pas poursuivre tant qu'une condition n'est pas remplie. Vous utiliserez alors un objet de condition pour gérer les threads qui ont acquis un verrou mais ne peuvent pas procéder à un travail utile.

  1. Précisons, notre simulation de banque. Il doit être impossible de faire sortir de l'argent d'un compte qui ne dispose pas de fonds suffisants. Sachez que nous ne pouvons pas utiliser un code de type :
    if (banque.getBalance(duCompte) >= montant)
       banque.transfert(duCompte, auCompte, montant);
  2. Il est tout à fait possible que le thread actuel soit désactivé entre le résultat positif du test et l'appel à la méthode transfert() :
    if (banque.getBalance(duCompte) >= montant)
       // le thread risque d'être désactivé ici
       banque.transfert(duCompte, auCompte, montant);
    D'ici à ce que le thread s'exécute à nouveau, le solde du compte risque d'être descendu sous le montant du retrait. Vous devez vous assurer qu'aucun autre thread ne puisse modifier le solde entre le test et le transfert. Pour ce faire, protégez le test et l'action de transfert avec un verrou :
    class Banque {
       private final double[] comptes;
       private Lock verrou = new ReentrantLock();  
    ...
       public void transfert(int de, int vers, double somme) {
          verrou.lock();
          while (compte[de] < somme) {
             System.out.print(Thread.currentThread());
             comptes[de] -= somme;
             System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
             comptes[vers] += somme;
             System.out.printf("  Balance totale : %10.2f €%n", getBalanceTotale());
          }
          verrou.unlock();
       }
    ...

    Que faire maintenant lorsqu'il n'y a pas suffisamment d'argent sur le compte ? Nous attendons qu'un autre thread ajoute des fonds. Mais ce thread vient d'obtenir un accès exclusif à verrou, aucun autre thread n'a donc l'occasion de réaliser un dépôt. C'est ici que les objets de condition entrent en jeu.

  3. Un objet verrou peut se voir associer un ou plusieurs objets de condition. Pour obtenir un objet de condition, utilisez la méthode newCondition(). Chaque objet de condition reçoit généralement un nom qui évoque la condition qu'il représente. Ici, par exemple, nous établissons un objet de condition pour représenter la condition "fonds suffisants".
    class Banque {
       private final double[] comptes;
       private Lock verrou = new ReentrantLock();  
       private Condition fondsSuffisants = verrou.newCondition();  
    ...
    
    Si la méthode transfert() découvre que les fonds ne sont pas suffisants, elle appelle la méthode await() :
    fondsSuffisants.await();      
    Le thread actuel est maintenant désactivé et abandonne le verrou, ce qui permet à un autre thread d'entrer et, nous l'espérons, augmenter le solde.
  4. Il existe une différence essentielle entre un thread qui attend d'acquérir un verrou et un thread qui a appelé la méthode await(). Lorsqu'un thread appelle la méthode await(), il entre un jeu d'attente pour cette condition. Le thread n'est pas rendu exécutable lorsque le verrou est disponible. Il reste désactivé jusqu'à ce qu'un autre thread appelle la méthode signalAll() sur la même condition.

    Ainsi, lorsqu'un autre thread tranfère de l'argent :
    fondsSuffisants.signalAll();      

    Cet appel réactive tous les threads qui attendent la condition. Lorsque les threads sont retirés du jeu d'attente, ils redeviennent exécutables et le gestionnaire finira par les réactiver. A ce moment là, ils tenterons de rentrer dans l'objet. Dès que le verrou est diponible, l'un d'entre eux acquerra le verrou et continuera là ou il s'est arrêté, en revenant à l'appel await().

    A ce moment là, le thread doit à nouveau tester la condition. Il n'est pas garanti que la condition soit remplie. La méthode signalAll() signale simplement aux threads en attente qu'elle peut être remplie à ce moment là et quil faut à nouveau vérifier la condition.

    En général, un appel à la méthode await() doit se trouver dans une boucle sous la forme :

    while (!(ok pour continuer)) 
        fondsSuffisants.await();      

    Il est particulièrement important qu'un seul thread appelle la méthode signalAll(). Lorsqu'un thread appelle la méthode await(), il n'a aucun moyen de le réactiver. Il met sa confiance dans les autres threads. Si aucun d'entre eux ne se préoccupe de réactiver le thread en attente, il ne sera jamais réexécuté. Cela peut mener à des situations déplaisantes de verrous morts. Si tous les autres threads sont bloqués et que le dernier thread actif appelle await() sans débloquer l'un des autres, il se bloque aussi. Aucun thread n'ayant l'autorisation de débloquer les autres, le programme plante.

    A quel moment appeler signalAll() ? En règle générale, il vaut mieux l'appeler dès que l'état d'un objet change d'une manière qui pourrait être avantageuse pour les threads qui sont en attente. Par exemple, dès qu'un solde banquaire change, les threads en attente doivent avoir une occasion d'inspecter le solde.

    Dans notre exemple, nous appelons la méthode signalAll() lorsque nous avons fini le transfert de fonds :
    class Banque {
       private final double[] comptes;
       private Lock verrou = new ReentrantLock();  
       private Condition fondsSuffisants = verrou.newCondition();
    ...
       public void transfert(int de, int vers, double somme) {
          verrou.lock();
          while (compte[de] < somme) fondsSuffisants.await();
          System.out.print(Thread.currentThread());
          comptes[de] -= somme;
          System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
          comptes[vers] += somme;
          System.out.printf("  Balance totale : %10.2f €%n", getBalanceTotale());
          fondsSuffisants.signalAll();  
          verrou.unlock();
       }
    ...
    Vous remarquez que l'appel à signalAll() n'active pas immédiatement un thread en attente. Il ne fait que débloquer les threads en attente de sorte qu'ils puissent entrer en concurrence pour entrer dans l'objet après que le thread actuel est sortie de la méthode synchronisée

    Une autre méthode, signal(), ne débloque qu'un thread choisi au hasard dans le jeu d'attente. Ceci est plus efficace que débloquer tous les threads, mais il y a un danger. Si le thread choisi aléatoirement découvre qu'il ne peut toujours pas poursuivre, il est à nouveau bloqué. Si aucun thread n'appelle signal(), le système génère des verrous morts.

L'interface java.util.concurrent.locks.Lock
Condition newCondition()
Renvoie un objet de condition associé à ce verrou.
L'interface java.util.concurrent.locks.Condition
void await()
Place ce thread dans le jeu d'attente pour cette condition.
void signalAll()
Débloque tous les threads du jeu d'attente pour cette condition.
void signal()
Débloque un thread choisi au hasard dans le jeu d'attente pour cette condition.
A titre d'exemple, reprenons notre banque en prenant en compte la notion de verrou et de condition. Ainsi, vous verrez que tout va bien. Le solde reste à 100 000 €. Aucun compte n'aura jamais de solde négatif. Vous pourrez aussi remarquer que le programme est un peu plus lent : c'est le prix à payer pour la charge supplémentaire impliquée par la synchronisation.

codage correspondant
package synchronisation;

import java.util.concurrent.locks.*;


public class Synchronisé {
   private static final int NOMBRE_COMPTE = 100;
   private static final double SOLDE_INITIAL = 1000.0;

   public static void main(String[] args) { 
      Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL);
      for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start();
   }
}

class Banque {
   private final double[] comptes;
   private Lock verrou = new ReentrantLock();
   private Condition fondsSuffisants = verrou.newCondition();

   public Banque(int nombre, double soldeInitiale) {
      comptes = new double[nombre];
      for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale;
   }

   public void transfert(int de, int vers, double somme) throws InterruptedException {
      verrou.lock();
      try {
         while (comptes[de] < somme)  fondsSuffisants.await();
         System.out.print(Thread.currentThread());
         comptes[de] -= somme;
         System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
         comptes[vers] += somme;
         System.out.printf("  Balance totale : %10.2f €%n", getBalanceTotale());
         fondsSuffisants.signalAll();       
      } 
      finally { verrou.unlock(); }
   }

   public double getBalanceTotale() {
      verrou.lock();
      try {
         double solde = 0.0;
         for (double compte : comptes) solde += compte;
         return solde;
      }
      finally { verrou.unlock(); }
   }

   public int nombreDeComptes() { return comptes.length; }
}

class Transfert implements Runnable {
   private Banque banque;
   private int duCompte;
   private double maximumAutorisé;

   public Transfert(Banque banque, int duCompte, double maximumAutorisé) {
      this.banque = banque;
      this.duCompte = duCompte;
      this.maximumAutorisé = maximumAutorisé;
   }

   public void run() {
      try {
         int versCompte = (int)(banque.nombreDeComptes() * Math.random());
         double montant = maximumAutorisé * Math.random();
         banque.transfert(duCompte, versCompte, montant);
         Thread.sleep((int)(10*Math.random()));
      }
      catch (InterruptedException e) {}
   }
}

Dans la pratique, une utilisation correcte des conditions peut être assez difficile. Avant de lancer l'implémentation de vos propres objets de condition, envisagez l'une des constructions décrites plus loin, dans la section sur la synchronisation.

Le mot clé synchronized

Dans les sections précédentes, vous avez vu comment utiliser Lock et les objets Condition. Avant de poursuivre, résumons les points principaux concernant les verrous et les conditions :

  1. Un verrou protège des sections de code, ne permettant qu'à un seul thread d'exécuter du code à un momment donné.
  2. Un verrou gère les threads qui tentent d'entrer dans un segment de code protégé.
  3. Un verrou peut se voir associer un ou plusieurs objets de condition.
  4. Chaque objet de condition gère les threads qui sont entrés dans une section de code protégée mais ne peuvent pas poursuivre.

Les interfaces Lock et Condition ont été ajoutées à Java SE 5.0 pour donner aux programmeurs un meilleur contrôle sur le vérouillage. Toutefois, la plupart du temps, ce contrôle est inutile et vous pouvez utiliser un mécanisme directement intégré dans le langage Java. En effet, depuis la version 1.0, chaque objet de Java possède un verrou intrinsèque. Si une méthode est déclarée avec le mot clé synchroniszed, le verrou d'objet protède toute la méthode. Pour appeler la méthode, un thread doit donc acquérir le verrou de l'objet intrinsèque.

Autrement dit :
public synchronized void uneMéthode() {
    // section principale
}
équivaut à :
public void uneMéthode() {
   verrou.lock();   // un objet ReentrantLock
   try {
      // section principale
   }
   finally {
      verrou.unlock();  // vérifier que le verrou est dévérouillé, même si une exception est déclenchée
   }
}

Par exemple, au lieu d'utiliser un verrou explicite, nous pouvons simplement déclarer comme synchronized la méthode transfert() de la classe Banque.

Le verrou d'objet intrinsèque ne s'est vu associer qu'une seule condition. La méthode wait() ajoute un thread au jeu d'attente et les méthodes notifyAll() et notify() débloquent les threads en attente.

Autrement dit, appelez wait() et notifyAll() équivaut respectivement à :

fondsSuffisants.await();
fondsSuffisants.signalAll(); 

Les méthodes wait() et notifyAll() ou notify() sont des méthodes final de la classe Object. Les méthodes de Condition ont dû être nommées await(), signal() et signalAll() pour ne pas rentrer en confilt avec les précédentes.

Ainsi, vous pouvez implémenter la classe Banque comme suit :
class Banque {
   private final double[] comptes;
...
   public synchronized void transfert(int de, int vers, double somme) {
      while (compte[de] < somme) wait(); // attend la seule condition du verrou de l'objet intrinsèque
      System.out.print(Thread.currentThread());
      comptes[de] -= somme;
      System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
      comptes[vers] += somme;
      System.out.printf("  Balance totale : %10.2f €%n", getBalanceTotale());
      notifyAll();  // avertir tous les threads attendant la condition
   }
...

Comme vous le voyez, l'utilisation du mot clé synchronized permet d'avoir un code bien plus concis. Pour le comprendre, vous devez savoir que chaque objet possède un verrou intrinsèque et que le verrou a une condition intrinsèque. Le verrou gère les threads qui tentent d'entrer dans une méthode synchronized. La condition gère les threads qui ont appelée la méthode wait().

Vous pouvez aussi déclarer une méthode statique comme synchronisée. Lorsque cette méthode est appelée, elle acquiert l'aspect intrinsèque de l'objet de classe associée. Par exemple, si la classe Banque possède une méthode synchronisée statique, le verrou de l'objet Banque.class est vérouillé lorsqu'il est appelé. En conséquence, aucun autre thread ne peut appeler cette méthode statique synchronisée de la même classe ou une autre.

Avantage et inconvénients des verrous intrinsèques

Les verrous intrinsèques et les conditions présentent certaines limites, et parmi elles :

  1. Vous ne pouvez pas interrompre un thread qui tente d'acquérir un verrou.
  2. Vous ne pouvez pas spécifier de temporisation lorsque vous tentez d'acquérir un verrou.
  3. Ne disposez que d'une seule condition par verrou peut se révéler insuffisant.

Nous pouvons alors nous interroger sur ce qu'il faut utiliser dans le code : des objets Lock et Condition ou des méthodes synchonisées ? Voici quelques conseils :

  1. Il vaut mieux n'utiliser ni Lock et Condition ni le mot clé synchonized. Dans de nombreux cas, vous pourrez utiliser l'un des mécanismes du paquetage java.util.concurrent qui gère le vérouillage. Vous verrez plus loin notamment comment utiliser une queue de blocage pour synchroniser des threads qui agissent sur une même tâche.
  2. Si le mot clé synchronized fonctionne pour vous, utilisez-le. Vous écrirez moins de code. Un code plus simple, comme dans le cas de la Banque, réduit les risquent d'erreur.
  3. Utilisez Lock et Condition si vous avez particulièrement besoin de la puissance offerte par ces constructions.
La classe java.lang.Object
void notifyAll()
Débloque les threads qui ont appelé wait() sur cet objet. Cette méthode ne peut être appelée que depuis une méthode synchronisée ou un bloc. La méthode déclenche une IllegalMonitorStateException si le thread courant n'est pas propriétaire du verrou d'objet.
void notify()
Débloque un thread choisi aléatoirement parmi ceux qui ont appelé wait() sur cet objet. Cette méthode ne peut être appelée que depuis une méthode synchronisée ou un bloc. La méthode déclenche une IllegalMonitorStateException si le thread courant n'est pas propriétaire du verrou d'objet.
void wait()
Amène un thread à patienter jusqu'à ce qu'il soit notifié. Cette méthode ne peut être appelée que depuis une méthode synchronisée. La méthode déclenche une IllegalMonitorStateException si le thread courant n'est pas propriétaire du verrou d'objet.
void wait()
void wait(long millisecondes, int nanosecondes)
Amène un thread à patienter jusqu'à ce qu'il soit notifié ou jusqu'à ce que le délai spécifié se soit écoulé. Ces méthodes ne peuvent être appelées que depuis une méthode synchronisée. Elles déclenchent une IllegalMonitorStateException si le thread courant n'est pas propriétaire du verrou d'objet.

* millisecondes : Le nombre de millisecondes
* nanosecondes : Le nombre de nanosecondes < 1 000 000.
Exemple complet de la banque prenant en compte les verrous intrinsèques
package synchronisation;

public class Synchronisé {
   private static final int NOMBRE_COMPTE = 100;
   private static final double SOLDE_INITIAL = 1000.0;

   public static void main(String[] args) { 
      Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL);
      for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start();
   }
}
class Banque {
   private final double[] comptes;

   public Banque(int nombre, double soldeInitiale) {
      comptes = new double[nombre];
      for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale;
   }

   public synchronized void transfert(int de, int vers, double somme) throws InterruptedException {
         while (comptes[de] < somme)  wait();
         System.out.print(Thread.currentThread());
         comptes[de] -= somme;
         System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
         comptes[vers] += somme;
         System.out.printf("  Balance totale : %10.2f €%n", getBalanceTotale());
         notifyAll();       
   }

   public synchronized double getBalanceTotale() {
         double solde = 0.0;
         for (double compte : comptes) solde += compte;
         return solde;
   }

   public int nombreDeComptes() { return comptes.length; }
}

class Transfert implements Runnable {
   private Banque banque;
   private int duCompte;
   private double maximumAutorisé;

   public Transfert(Banque banque, int duCompte, double maximumAutorisé) {
      this.banque = banque;
      this.duCompte = duCompte;
      this.maximumAutorisé = maximumAutorisé;
   }

   public void run() {
      try {
         int versCompte = (int)(banque.nombreDeComptes() * Math.random());
         double montant = maximumAutorisé * Math.random();
         banque.transfert(duCompte, versCompte, montant);
         Thread.sleep((int)(10*Math.random()));
      }
      catch (InterruptedException e) {}
   }
}

Blocs synchronisés

Nous venons de le voir, chaque objet Java possède un verrou en appelant une méthode synchronisée. Il existe un second mécanisme qui réalise le même type de service, en utilisant cette fois-ci le bloc synchronisé.

Si un thread entre dans un bloc de la forme présentée ci-dessous, il acquiert le verrou pour objet. Le verrou est rentrant.

synchronized (objet)  { // syntaxe pour un bloc synchonisé

    // section principale
}

Vous trouverez souvent des verrous ad hoc dans un code existant, comme :

class Banque {
   private final double[] comptes;
   private Object verrou = new Object();
...
   public  void transfert(int de, int vers, double somme) {
...
      synchronized (verrou) {
         comptes[de] -= somme;
         comptes[vers] += somme;
      }
...
   }

Ici, l'objet verrou n'est créé que pour utiliser le verrou que possède chaque objet Java.
.

Le concept des moniteurs

Les verrous et les conditions constituent des outils puissants pour la synchronisation des threads, mais ils ne sont pas vraiment orientés objet. L'une des solutions les plus importantes est le concept de moniteur.

En terminologie Java, un moniteur présente les propriétés suivantes :

  1. Il est une classe n'ayant que des attributs privés.
  2. Chaque objet de cette classe se voit associé un verrou.
  3. Toutes les méthodes sont vérouillées pas ce verrou. Autrement dit, si un client appelle objet.méthode(), le verrou de objet est automatiquement acquis au début de l'appel de méthode et abandonné à la fin. Tous les attributs sont privés, cet arrangement permet d'assurer qu'aucun thread n'y accède, alors qu'un autre thread les manipule.
  4. Le verrou peut posséder n'importe quel nombre de conditions associées.

Les concepteurs de Java ont adapté le concept du moniteur. Chaque objet de Java possède un verrou intrinsèque et une condition intrinsèque. Si une méthode est déclarée avec le mot réservée synchronized, elle agit comme une méthode moniteur. La variable de condition est accessible par un appel à wait()/notify()/notifyAll().

Toutefois, un objet Java diffère d'un moniteur en trois points importants, qui compromettent la sécurité des threads :

  1. Les attributs n'ont pas besoin d'être privés.
  2. Les méthodes n'ont pas besoin d'être synchronized.
  3. Le verrou intrinsèque est disponible pour les clients.

Test de verrous et temporisations

Un thread se bloque de manière indéfinie lorsqu'il appelle la méthode lock() pour acquérir un verrou appartenant à un autre thread. Vous pouvez prendre des précautions supplémentaires pour acquérir un verrou.

La méthode tryLock() tente ainsi d'acquérir un verrou et renvoie true si elle a réussi, sinon, elle renvoie immédiatement false et le thread peut s'arrêter pour faire autre chose :

public void uneMéthode() {
   if  (verrou.tryLock()) {
     // le thread possède alors le verrou
     try { ... }
     finally { verrou.unlock(); }
   else  // faire autre chose
}
Vous pouvez également appelez la méthode tryLock() avec un paramètre de temporisation, comme ceci :

public void uneMéthode() {
   if  (verrou.tryLock(100, TimeUnit.MILLISECONDS)) {
     // le thread possède alors le verrou
     try { ... }
     finally { verrou.unlock(); }
   else  // faire autre chose
}

TimeUnit est une énumération ayant les valeurs SECONDS, MILLISECONDS, MICROSECONDS et NANOSECONDS.
§

La méthode lock() ne peut pas être interrompue. Si un thread est interrompu lorsqu'il attend d'acquérir un verrou, il reste bloqué jusqu'à ce que le verrou soit disponible. En cas de verrou mort, la méthode lock() ne peut jamais se terminer.

Toutefois, si vous appelez tryLock() avec une temporisation, une InterruptedException est déclenchée si le thread est interrompu pendant qu'il attend. C'est particulièrement utile car cela permet à un programme de casser les verrous morts.

Il existe d'autres méthodes qui permettent de prendre en compte la notion de temps :

  1. lockInterruptibly() : Vous pouvez également appeler la méthode lockInterruptibly(). Elle a la même signification que tryLock() avec une temporisation infinie.
  2. await() : Lorsque vous attendez une condition, il est enfin possible d'apporter une temporisation :
    
    condition.await(100, TimeUnit.MILLISECONDS);

    La méthode await() se termine si un autre thread a activé ce thread en appelant signalAll() ou signal() ou si la temporisation s'est achevée ou si le thread a été interrompu.

    Les méthodes await() déclenchent une InterruptedException si le thread en attente est interrompu.
    §

  3. awaitUninterruptibly() : Dans le cas (peu probable) où vous préféreriez continuer à attendre, utilisez la méthode awaitUninterruptibly().
L'interface java.util.concurrent.locks.Lock
boolean tryLock()
Tente d'acquérir le verrou sans bloquer ; renvoie true si c'est réussi. Cette méthode saisit le verrou s'il est disponible même s'il possède une règle de vérouillage équitable et que d'autres threads attendent.
boolean tryLock(long durée, TimeUnit unitéDeTemps)
Tente d'acquérir le verrou, en le bloquant au maximum le temps donné ; renvoie true si c'est réussi.
void lockInterruptibly()
Acquiert le verrou, en le bloquant indéfiniment. Si le thread est interrompu, déclenche une InterruptedException.
L'interface java.util.concurrent.locks.Condition
boolean await(long durée, TimeUnit unitéDeTemps)
Entre le jeu d'attente pour cette condition, en bloquant jusqu'à ce que le thread soit retiré du jeu d'attente ou que le delai soit écoulé. Renvoie false si la méthode est renvoyée car le délai s'est écoulé, true dans les autres cas.
void awaitUninterruptibly()
Entre le jeu d'attente pour cette condition, en bloquant jusqu'à ce que le thread soit retiré du jeu d'attente. Si le thread est interrompu, cette méthode ne déclenche pas d'InterruptedException.

Lire et écrire des verrous

Le paquetage java.util.concurrent.locks définit deux classes de verrou, ReentranLock, que nous connaissons déjà, et la classe ReentrantReadWriterLock. Cette dernière sert lorsque trop de thread lisent à partir d'une structure de données et qu'il existe moins de threads pour les modifier.

Dans cette situation, il est logique d'autoriser l'accès pour les lecteurs. Bien entendu, un système d'écriture doit toujours avoir un accès exclusif.
§

Voici les étapes nécessaires pour utiliser la lecture et l'écriture de verrous :

  1. Construisez un objet de type ReentrantReadWriterLock :
    class Banque {
    ...
       private ReentrantReadWriteLock verrou = new ReentrantReadWriteLock(); 
    ...
    
  2. Extrayer les verrous de lecture et d'écriture :
    class Banque {
    ...
       private ReentrantReadWriteLock verrou = new ReentrantReadWriteLock();
       private Lock verrouEnLecture = verrou.readLock();
       private Lock verrouEnEcriture = verrou.writeLock();
    ...
    
  3. Utilisez le verrou de lecture dans toutes les méthodes d'accès :
    ...
    
       public double getBalanceTotale() {
          verrouEnLecture.lock();
          try { ... }
          finally { verrouEnLecture.unlock(); }
       }
    ...
  4. Utilisez le verrou d'écriture dans toutes les méthodes de modification :
    ...
    
       public void transfert(int de, int vers, double somme) {
          verrouEnEcriture.lock();
          try { ... } 
          finally { verrouEnEcriture.unlock(); }
       }
      ...
La classe java.util.concurrent.locks.ReentrantReadWriteLock
Lock readLock()
Récupère un verrou de lecture pouvant être acquis par plusieurs lecteurs, hors systèmes d'écriture.
Lock writeLock()
Récupère un verrou d'écriture qui exclut tous les lecteurs et systèmes d'écriture.
A titre d'exemple, reprenons notre banque en prenant en compte la notion de lecture et d'écriture de verrou
package synchronisation;

import java.util.concurrent.locks.*;

public class Synchronisé {
   private static final int NOMBRE_COMPTE = 100;
   private static final double SOLDE_INITIAL = 1000.0;

   public static void main(String[] args) { 
      Banque banque = new Banque(NOMBRE_COMPTE, SOLDE_INITIAL);
      for (int i=0; i<NOMBRE_COMPTE; i++) new Thread(new Transfert(banque, i, SOLDE_INITIAL)).start();
   }
}

class Banque {
   private final double[] comptes;
   private ReentrantReadWriteLock verrou = new ReentrantReadWriteLock();
   private Lock verrouEnLecture =verrou.readLock();
   private Lock verrouEnEcriture =verrou.writeLock();
   private Condition fondsSuffisants = verrouEnEcriture.newCondition();

   public Banque(int nombre, double soldeInitiale) {
      comptes = new double[nombre];
      for (int i=0; i<nombre; i++) comptes[i] = soldeInitiale;
   }

   public void transfert(int de, int vers, double somme) throws InterruptedException {
      verrouEnEcriture.lock();
      try {
         while (comptes[de] < somme)  fondsSuffisants.await();
         System.out.print(Thread.currentThread());
         comptes[de] -= somme;
         System.out.printf(" %10.2f € de %d à %d", somme, de, vers);
         comptes[vers] += somme;
         System.out.printf("  Balance totale : %10.2f €%n", getBalanceTotale());
         fondsSuffisants.signalAll();       
      } 
      finally { verrouEnEcriture.unlock(); }
   }

   public double getBalanceTotale() {
      verrouEnLecture.lock();
      try {
         double solde = 0.0;
         for (double compte : comptes) solde += compte;
         return solde;
      }
      finally { verrouEnLecture.unlock(); }
   }

   public int nombreDeComptes() { return comptes.length; }
}

class Transfert implements Runnable {
   private Banque banque;
   private int duCompte;
   private double maximumAutorisé;

   public Transfert(Banque banque, int duCompte, double maximumAutorisé) {
      this.banque = banque;
      this.duCompte = duCompte;
      this.maximumAutorisé = maximumAutorisé;
   }

   public void run() {
      try {
         int versCompte = (int)(banque.nombreDeComptes() * Math.random());
         double montant = maximumAutorisé * Math.random();
         banque.transfert(duCompte, versCompte, montant);
         Thread.sleep((int)(10*Math.random()));
      }
      catch (InterruptedException e) {}
   }
}

 

Choix du chapitre Pourquoi les méthodes stop() et suspend() ne sont plus utilisées

La première version de Java a défini une méthode stop() qui se contente de mettre fin à un thread et une méthode suspend() qui bloque un thread jusqu'à ce qu'un autre thread appelle resume(). Ces deux méthodes ont quelque chose en commun : elles tentent de contrôler le comportement d'un thread donné sans sa coopération.

Ces deux méthodes ne sont plus utilisées depuis Java SE 1.2. La méthode stop() est intrinsèquement non sûre et l'expérience a montré que la méthode suspend() amène souvent à des situations de verrous morts. Dans cette section, nous allons voir pourquoi ces méthodes posent des problèmes, et ce que nous pouvons faire pour les éviter.

La méthode stop()

Cette méthode met fin à toutes les méthodes en attente, y compris la méthode run(). Lorsqu'un thread est arrêté, il rend immédiatement les verrous sur tous les objets qu'il a vérouillés. Cela peut laisser les objets dans un état incohérent.

Par exemple, supposons qu'un thread de transfert soit arrêté en plein milieu d'un déplacement d'argent d'un compte vers un autre, après le retrait et avant le dépôt. L'objet de banque est alors endommagé. Le verrou ayant été abandonné, les dommages peuvent être constatés sur les autres threads qui n'ont pas été arrêtés.

Lorsqu'un thread veut arrêter un autre thread, il n'a aucun moyen de savoir à quel moment l'emploi de la méthode stop() est sûr, et à quel moment il risque d'endommager des objets. Par conséquent cette méthode n'est plus utilisée. Il est conseillé d'interrompre un thread que lorsque vous voulez l'arrêter. Le thread interrompu peut alors s'arrêter au moment opportun.

La méthode suspend()

Contrairement à stop(), la méthode suspend() ne peut pas endommager les objets. Cependant, si vous suspendez un thread qui possède un verrou sur un objet, cet objet ne sera plus disponible jusqu'à ce que le thread ait repris son activité.

Si le thread qui appelle la méthode suspend() essaie d'obtenir le verrou sur le même objet, le programme se bloque : le thread interrompu attend qu'on lui demande de reprendre son activité et le thread qu'il a mis en attente attend que l'objet soit dévérouillé.

Cette situation se produit fréquemment avec des interfaces utilisateurs graphiques. Supposons que nous ayont une simulation graphique de notre banque. Nous disposons d'un bouton "Pause" qui suspend les threads de transfert et d'un bouton "Continu" qui les fait continuer :
boutonPause.addActionListener(new ActionListener() {
   public void actionPerformed(ActionEvent événement) {
       for (int i=0; i<threads.lenght; i++)
          threads[i].suspend() // Surtout ne pas faire cela
   }
});
boutonContinu.addActionListener(...);  // appelle resume() sur tous les threads de transfert

Une méthode paintComponent() dessine un graphique pour chaque compte, en appelant la méthode getBalanceTotale().
.

Comme nous le verrons plus tard, les actions des boutons et l'action graphique se trouve dans le même thread, appelé thread de répartition des événements.

Considérons maintenant le scénario suivant :

  1. L'un des threads de transfert obtient un verrou sur l'objet banque.
  2. L'utilisateur clique sur le bouton "Pause".
  3. Tous les threads de transfert sont suspendus ; l'un d'entre eux possède toujours le verrou sur l'objet banque.
  4. Pour une certaine raison le graphique doit être redessiné.
  5. La méthode paintComponent() appelle donc la méthode getBalanceTotal().
  6. Cette méthode tente d'acquérir le verrou de l'objet banque.

Le programme est alors bloqué : Le thread de répartition des événements ne peut pas continuer parce que ce verrou appartient à l'un des threads suspendus. Par conséquent, l'utilisateur ne peut pas cliquer sur le bouton "Continu", et aucun thread ne continuera son exécution.

Si vous souhaitez interrompre un thread de manière sûre, il convient d'introduire une variable demandeArrêt et de la tester dans un lieu sûr de votre méthode run(), où votre thread ne vérouille pas des objets dont d'autres threads ont besoin. Lorsque cette variable a été définie, continuez à attendre jusqu'à ce qu'elle soit à nouveau disponible.
public void run() {
   while (...)
      ...
      if (demandeArrêt) {
          verrou.lock();
          try { while (demandeArrêt)  condition.await(); }
          finally { verrou.unlock(); }
      }
   }
}

private boolean demandeArrêt = false;
private Lock verrou = new ReentrantLock();
private Condition condition = verrou.newCondition();

 

Choix du chapitre Queues de blocage

Nous venons de voir les blocs de base de la programmation simultanée en Java. Toutefois, pour la programmation pratique, tenez-vous-en éloigné chaque fois que possible. Il est bien plus facile et sûr d'utiliser des structures de niveau supérieur implémentées par des experts de la simultanéité.

De nombreux problèmes de thread peuvent être formulés de manière élégante et sûre à l'aide d'une ou de plusieurs queues. Les threads de producteurs insèrent des éléments dans la queue, puis les threads de consommation les récupèrent. La queue permet de transférer en toute sécurité des données d'un thread à l'autre.

Etudions, par exemple, notre programme de virement bancaire. Plutôt que d'accéder à l'objet banque directement, les threads de transfert insère des objets d'instruction dans une queue. Un autre thread supprime les instructions de la queue et réalise les transferts.

Seul ce thread a accès aux coulisses de l'objet banque. Aucune synchronisation n'est nécessaire (bien entendu, les implémenteurs des classes de queue compatibles avec les threads ont dû s'inquiéter des verrous et des conditions, mais c'était leur problème, ne vous en préoccuper pas).

Coordination du travail

Une queue de blocage amène un thread à se bloquer lorsque vous tentez d'ajouter un élément alors que la queue est pleine ou de supprimer un élément alors qu'elle est vide. Les queues de blocages sont un outil utile pour coordonner le travail de plusieurs threads. Des threads de travail suppriment les résultats intermédiaires et les modifient ensuite. La queue équilibre automatiquement la charge de travail.

Si le premier ensemble de threads s'exécute plus lentement que le second, celui-ci se bloque en attendant les résultats. Si le premier jeu de thread s'exécute plus rapidement, la queue se remplit jusqu'à ce que le second ensemble rattrape son retard.

Voici la liste des méthodes relatives aux queues de blocage :

  1. add() : Ajoute un élément : Déclenche une IllegalStateException si la queue est pleine.
  2. remove() : Supprime et renvoie l'élément de tête : Déclenche une NoSuchElementException si la queue est vide.
  3. element() : Renvoie l'élément de tête : Déclenche une NoSuchElementException si la queue est vide.
  4. offer() : Ajoute un élément et renvoie true : Renvoie false si la queue est pleine.
  5. poll() : Supprime et renvoie l'élément de tête : Renvoie null si la queue était vide.
  6. peek() : Renvoie l'élément de tête : Renvoie null si la queue était vide.
  7. put() : Ajoute un élément : Bloque si la queue est pleine.
  8. take() : Supprime et renvoie la tête : Bloque si la queue était vide.

Les méthodes poll() et peek() renvoient null pour signaler une erreur. L'insertion de valeurs null dans ces queues n'est donc pas autorisée.
§

Fonctionnalité des queues de blocage

Le fonctionnement des queues de blocage est réparti en trois catégories, selon leur action lorsque la queue est pleine ou vide :

  1. Si vous utilisez la queue comme outil de gestion de threads, vous devrez utiliser les méthodes put() et take().
  2. Les opérations add(), remove() et element() déclenchent une exception lorsque vous tentez d'ajouter des éléments à une queue pleine ou de récupérer la tête d'une queue vide.
  3. Bien entendu, dans un programme multithread, la queue peut se remplir ou se vider à tout moment, il faudra donc plutôt utiliser les méthodes offer(), poll() et peek(). Elles renvoient simplement un indicateur d'échec au lieu de déclencher une exception si elles ne peuvent pas réaliser leurs tâches.
Il existe aussi des variantes des méthodes offer() et poll(). L'exemple qui suit tentent pendant 100 millisecondes d'insérer un élément à la queue de la file au moyen de la méthode offer(). S'il réussit, il renvoie immédiatement true, sinon il renvoie false à la fin de la temporisation. De même que l'appel à la méthode poll() renvoie true pendant 100 millisecondes pour supprimer la tête de la file. S'il réusssit, il renvoie la tête, sinon il renvoie null à la fin de la temporisation :
boolean succès = queue.offer(x, 100, TimeUnit.MILLISECONDS);
Object tête = queue.poll(100, TimeUnit.MILLISECONDS);

La méthode put() bloque si la queue est pleine, la méthode take() bloque si la queue est vide. Ce sont les équivalents d'offer() et poll(), sans temporisation.
§

Les différentes queues de blocage

Le paquetage java.util.concurrent propose plusieurs variations des queues de blocage :

  1. Par défaut, LinkedBlockingQueue ne possède pas de bornes supérieures quant à sa capacité, mais une capacité maximale peut être spécifiée en option.
  2. LinkedBlockingDeque est une version à double extrémité.
  3. ArrayBlockingQueue se construit avec une capacité donnée et un paramètre optionnel pour obliger à l'équité.

    Si l'équité est spécifiée, les threads qui ont attendu le plus longtemps reçoivent un traitement préférentiel. Comme toujours, l'équité pénalise considérablement les performances (ne l'utilisez que si vos problèmes l'exigent spécifiquement).

  4. PriorityBlockingQueue est une queue prioritaire. Les éléments sont supprimés dans l'ordre de leur priorité. La queue affiche une capacité sans limites, mais la récupération bloquera si la queue est vide.

    Revoir l'étude sur les collections pour en savoir plus sur les queues de priorité.
    §

  5. Enfin, un DelayQueue contient des objets qui implémentent l'interface Delayed. La méthode getDelay() de cette interface renvoie le délai restant de l'objet. Une valeur négative indique un délai écoulé. Les éléments ne peuvent être supprimés d'un DelayQueue que si leur délai a expiré. Vous devez aussi implémenter la méthode compareTo(). DelayQueue utilise cette méthode pour trier les entrées.
    interface Delayed extends Comparable<Delayed> {
       long getDelay(TimeUnit unité);
    }
La classe java.util.concurrent.ArrayBlockingQueue<E>
ArrayBlockingQueue(int capacité)
ArrayBlockingQueue(int capacité, boolean équité)
Construisent une queue de blocage avec la capacité donnée et des paramètres d'équité. La queue est implémentée sous forme de tableau circulaire.
La classe java.util.concurrent.LinkedBlockingQueue<E>
La classe java.util.concurrent.LinkedBlockingDeque<E>
ArrayBlockingQueue()
ArrayBlockingDeque()
Construisent une queue ou deque de blocage sans bornes, implémentées sous forme de liste chaînée.
ArrayBlockingQueue(int capacité)
ArrayBlockingDeque(int capacité)

Construisent une queue ou deque de blocage avec la capacité donnée, implémentées sous forme de liste chaînée.

La classe java.util.concurrent.DelayQueue<E extends Delayed>
DelayQueue()
Construit une queue de blocage d'éléments Delayed. Seules les éléments dont le délai a expiré seront supprimé de la queue.
L'interface java.util.concurrent.Delayed
long getDelay(TimeUnit unitéDeTemps)
Récupère le délai pour cet objet, mesuré dans l'unité de temps donné.
La classe java.util.concurrent.PriorityBlockingQueue<E>
PriorityBlockingQueue()
PriorityBlockingQueue(int capacité)
PriorityBlockingQueue(int capacité, Comparator<? super E> comparateur)
Construisent une queue d'attente de priorité de blocages sans bornes, implémentée sous forme de tas. La capacité initiale de la queue de priorité vaut 11 par défaut. Si le comparateur n'est pas spécifié, les éléments doivent implémenter l'interface Comparable.
La classe java.util.concurrent.BlockingQueue<E>
void put(E élément)
Ajoute l'élément, blocage si nécessaire.
E take()
Supprime et renvoie l'élément de tête, blocage si nécessaire.
boolean offer(E élément, long durée, TimeUnit unitéDeTemps)
Ajoute l'élément et renvoie true en cas de réussite, blocage si nécessaire jusqu'à ce que l'élément ait été ajouté ou que le délai soit écoulé.
E poll(long durée, TimeUnit unitéDeTemps)
Supprime et renvoie l'élément de tête, blocage si nécessaire jusqu'à ce qu'un élément soit disponible ou que le délai soit écoulé. Renvoie null en cas d'échec.
La classe java.util.concurrent.BlockingDeque<E>
void putFirst(E élément)
void putLast(E élément)
Ajoute l'élément, blocage si nécessaire.
E takeFirst()
E takeLast()
Supprime et renvoie l'élément de tête, blocage si nécessaire.
boolean offerFirst(E élément, long durée, TimeUnit unitéDeTemps)
boolean offerLast(E élément, long durée, TimeUnit unitéDeTemps)
Ajoute l'élément et renvoie true en cas de réussite, blocage si nécessaire jusqu'à ce que l'élément ait été ajouté ou que le délai soit écoulé.
E pollFirst(long durée, TimeUnit unitéDeTemps)
E pollLast(long durée, TimeUnit unitéDeTemps)
Supprime et renvoie l'élément de tête, blocage si nécessaire jusqu'à ce qu'un élément soit disponible ou que le délai soit écoulé. Renvoie null en cas d'échec.

Exemple de mise en oeuvre

Le programme ci-dessous montre l'utilisation d'une queue de blocage qui contrôle un ensemble de threads. Le programme cherche dans les fichiers d'un répertoire et de ses sous-répertoire et affiche les lignes qui contiennent le mot spécifié.

  1. Un thread producteur recense tous les fichiers de tous les sous-répertoires et les place dans une queue de blocage. Cette opération est rapide et la queue se remplirait rapidement de tous les fichiers du système si elle n'était pas limitée.
  2. Nous démarrons également un grand nombre de threads de recherche. Chacun prend un fichier de la queue, l'ouvre, affiche toutes les lignes contenant le mot clé, puis prend le fichier suivant.
  3. Nous faisons appel à une astuce pour mettre fin à l'application lorsque le travail n'est plus nécessaire. Pour signaler la fin, le thread d'énumération place un objet vierge dans la queue. Lorsqu'un thread de recherche prend l'objet vierge, il le replace et se termine.

Sachez que la synchronisation explicite des threads n'est pas nécessaire. Dans cette application, nous utilisons la structure de données de la queue comme mécanisme de synchronisation.

Codage correspondant
package synchronisation;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

public class QueueDeBlocage {
   private static final int TAILLE_QUEUE = 10;
   private static final int THREADS_DE_RECHERCHE = 100;
   private static String répertoireDeBase = "D:/Developpez.com";

   public static void main(String[] args) {      
      Scanner clavier = new Scanner(System.in);
      System.out.print("Le mot à rechercher : ");
      String motDeRecherche = clavier.nextLine();
     
      BlockingQueue<File> queue = new ArrayBlockingQueue<File>(TAILLE_QUEUE);

      EnumérationFichiers fichiers = new EnumérationFichiers(queue, new File(répertoireDeBase));
      new Thread(fichiers).start();
      for (int i = 1; i <= THREADS_DE_RECHERCHE; i++)
         new Thread(new TâcheDeRecherche(queue, motDeRecherche)).start();
   }
}

class EnumérationFichiers implements Runnable {
   private BlockingQueue<File> queue;
   private File répertoire;
   public static File VIERGE = new File("");
   
   public EnumérationFichiers(BlockingQueue<File> queue, File répertoire) {
      this.queue = queue;
      this.répertoire = répertoire;
   }

   public void run() {
      try {
         énumère(répertoire);
         queue.put(VIERGE);
      }
      catch (InterruptedException e) {}
   }

   public void énumère(File répertoire) throws InterruptedException {
      File[] fichiers = répertoire.listFiles();
      for (File fichier : fichiers) {
          if (fichier.isDirectory()) énumère(fichier);
          else {
             queue.put(fichier);
             System.out.printf("Lecture du fichier %s\n", fichier.getPath());
          }
      }
   }
}

class TâcheDeRecherche implements Runnable {
   private BlockingQueue<File> queue;
   private String mot;
   
   public TâcheDeRecherche(BlockingQueue<File> queue, String mot) {
      this.queue = queue;
      this.mot = mot;
   }

   public void run() {
      try {
         boolean fini = false;
         while (!fini) {
            File fichier = queue.take();
            if (fichier == EnumérationFichiers.VIERGE) {
               queue.put(fichier);
               fini = true;
            }
            else recherche(fichier);
         }
      }
      catch (IOException e) { e.printStackTrace(); }
      catch (InterruptedException e) { }
   }

   public void recherche(File fichier) throws IOException {
      Scanner lecture = new Scanner(new FileInputStream(fichier));
      int nombreLigne = 0;
      while (lecture.hasNextLine()) {
         nombreLigne++;
         String ligne = lecture.nextLine();
         if (ligne.contains(mot))
            System.out.printf("%s:%d:%s%n", fichier.getPath(), nombreLigne, ligne);
      }
      lecture.close();
   }
}

 

Choix du chapitre Collections compatibles avec les threads

Si plusieurs threads modifient simultanément une structure de données, telle qu'une table de hachage, cette structure de données peut facilement être endommagée.

Un thread pourrait, par exemple, commencer à insérer un nouvel élément. Supposons qu'il soit alors préempté au milieu du réacheminement des liens entre les seaux de la table de hachage. Si un autre thread commence à parcourir la même liste, il risque de suivre des liens non valides et de créer un déséquilibre, par exemple en lançant des exceptions ou en s'engageant dans une boucle sans fin.

Pour en savoir plus sur les seaux et les tables de hachage.
§

Vous pouvez protéger une structure de données partagées en fournissant un verrou, mais il est généralement plus simple de choisir une implémentation compatible avec les threads. Les queues de blocages vues précédemment sont, bien entendu, des collections compatibles avec les threads. Dans cette section, nous en verrons d'autres proposées par la bibliothèque Java.

Cartes, jeux et queues efficaces

Le paquetage java.util.concurrent apporte des implémentations efficaces pour les cartes, les jeux triés et les queues :

  1. ConcurrentHashMap,
  2. ConcurrentSkipListMap,
  3. ConcurrentSkipListSet et
  4. ConcurrentLinkedQueue.

Ces collections utilisent des algorithmes complexes qui réduisent la contention en permettant un accès simultané à plusieurs parties de la structure de données.

A la différence de la plupart des collections, la méthode size() ne fonctionne pas forcément en temps constant. Déterminer la taille actuelle de l'une de ces collections nécessite généralement de les parcourir.

Au contraire, un itérateur d'une collection du paquetage java.util déclenche une ConcurrentModificationException lorsque la collection a été modifié après la construction de l'itérateur.

Table de hachage simultanée

La table de hachage simultanée peut efficacement supporter plusieurs lecteurs et un nombre de système d'écriture fixe. Par défaut, il peut exister jusqu'à 16 threads d'écriture simultanés. Nous pouvons avoir plus de threads d'écriture mais si plus de seize écrivent en même temps, les autres sont temporairement bloqués. Vous pouvez spécifier un nombre supérieur dans le constructeur mais il est certainement peu probable que vous deviez le faire.

Méthodes utiles

Les classes ConcurrentHashMap et ConcurrentSkipListMap possèdent des méthodes utiles pour des associations atomiques d'insertion et de retrait :

  1. putIfAbsent() : ajoute automatiquement une nouvelle association à condition qu'il n'y en ait pas eu avant. C'est utile pour un cache auquel accèdent plusieurs threads, pour s'assurer qu'un seul d'entre eux y ajoute un élément.
    cache.putIfAbsent(clé, valeur);
  2. remove() : L'opération opposée est proposée par la méthode remove() (qui aurait pu être appelée removeIfPresent()). L'appel à cette méthode supprime automatiquement la clé et la valeur si elles sont présentes dans la carte :
    cache.remove(clé, valeur);
  3. replace() : remplace automatiquement l'ancienne valeur par une nouvelle, à condition que l'ancienne valeur ait été associée à la clé spécifiée :
    cache.replace(clé, ancienneValeur, nouvelleValeur);
La classe java.util.concurrent.ConcurrentLinkedQueue<E>
ConcurrentLinkedQueue<E>()
Construit une queue sans limite ni blocage accessible en toute sécurité par plusieurs threads.
La classe java.util.concurrent.ConcurrentSkipListSet<E>
ConcurrentSkipListSet<E>()
ConcurrentSkipListSet<E>(Comparator<? super E> comparateur)
Construisent un jeu trié accessible en toute sécurité par plusieurs threads. Le premier constructeur exige que les éléments implémentent l'interface Comparable.
La classe java.util.concurrent.ConcurrentHashMap<Key, Value>
La classe java.util.concurrent.ConcurrentSkipListMap<Key, Value>
ConcurrentHashMap<Key, Value>()
ConcurrentHashMap<Key, Value>(int capacitéInitiale)
ConcurrentHashMap<Key, Value>(int capacitéInitiale, float facteurDeCharge, int niveauDeConcurrence)
Construisent une carte de hachage qui est accessible en toute sécurité par plusieurs threads.
° capacitéInitiale : Capacité initiale pour cette collection. Vaut 16 par défaut.
° facteurDeCharge : Contrôle le redimensionnement : si la charge moyenne par seau dépasse ce facteur, la table est redimensionnée. Vaut 0,75 par défaut.
°niveauDeConcurrence : Nombre estimé de thread d'écriture simultané.
ConcurrentSkipListMap<Key, Value>()
ConcurrentSkipListMap<Key, Value>(Comparator<? super E> comparateur)
Construisent une carte triée accessible en toute sécurité par plusieurs threads. Le premier constructeur exige que les clés implémentent l'interface Comparable.
Value putIfAbsent(Key clé, Value valeur)
Si la clé n'est pas encore présente dans la carte, associe la valeur donnée à la clé donnée et renvoie null, sinon renvoie la valeur existante associée à la clé.
boolean remove(Key clé, Value valeur)
Si la clé donnée est déjà associée à cette valeur, supprime la clé et la valeur données et renvoie true, sinon renvoie false.
boolean replace(Key clé, Value ancienneValeur, Value nouvelleValeur)
Si la clé donnée est actuellement associée à ancienneValeur, l'associe à nouvelleValeur, sinon renvoie false.

Copies de tableaux

CopyOnWriteArray et CopyOnWriteArraySet sont des collections compatibles avec les threads, dans lesquelles toutes les méthodes de modification créent une copie du tableau sous-jacent. Cet arrangement est utile lorsque le nombre de threads à parcourir la collection dépasse de loin le nombre de ceux qui la modifient.

Lorsque vous construisez un itérateur, il contient une référence au tableau actuel. Si le tableau est modifié par la suite, l'itérateur possède toujours l'ancien tableau, mais celui de la collection est remplacé. Par conséquent, l'itérateur le plus ancien possède une vue cohérente (mais peut-être obsolète) à laquelle il peut accéder sans frais de synchronisation.

 

Choix du chapitre Callable et Future

Un Runnable encapsule une tâche qui s'exécute de manière asynchrone. C'est en quelque sorte une méthode asynchrone sans paramètres ni valeur de retour.

Callable

Un Callable est identique, mais il renvoie une valeur. L'interface Callable est un type à paramètres, avec une seule méthode call() :

public interface Callable<Value> {
   Value call() throws Exception;
}

Le paramètre de type correspond au type de la valeur renvoyée. Par exemple Callable<Integer> représente un calcul asynchrone qui finit par renvoyer un entier (objet Integer).

Future

Un Future contient le résultat d'un calcul asynchrone. Un Future permet de démarrer un calcul, de donner le résultat à quelqu'un, puis de l'oublier. Lorsqu'il est prêt, le propriétaire de l'objet Future peut obtenir le résultat.

L'interface Future possède les méthodes suivantes :

public interface Future<Value> {
   Value get() throws ...;
   Value get(long durée, TimeUnit unitéDeTemps) throws ...;
   boolean cancel(Boolean peutÊtreInterrompu);
   boolean isCancelled();
   boolean isDone();
}
  1. get(...) : Un appel à la première méthode get() bloque jusqu'à ce que le calcul soit terminé. La deuxième méthode déclenche une TimeoutException si l'appel est arrivé à expiration avant la fin du calcul. Si le thread exécutant le calcul est interrompu, les deux méthodes déclenchent une InterruptedException. Si le calcul est terminé, get() prend fin immédiatement.
  2. isDone() : Cette méthode renvoie false si le calcul se poursuit, true s'il est terminé.
  3. cancel() : Vous pouvez annuler le calcul avec cette méthode. Si le calcul n'a pas commencé, il est annulé et ne commencera jamais. Si le calcul est en cours, il est interrompu lorsque le paramètre peutÊtreInterrompu vaut true.

FutureTask

L'emballage FutureTask est un mécanisme commode pour transformer un Callable à la fois en Future et en Runnable : il implémente les deux interfaces. Par exemple :

Callable<Integer> commutateur = ...;
FutureTask<Integer> question = new FutureTask<Integer>(commutateur);
Thread tâche = new Thread(question); // C'est un Runnable
tâche.start();
...
int résultat = question.get(); // C'est un Future
L'interface java.util.concurrent.Callable<Value>
Value call();
Exécute une tâche qui produit un résultat.
L'interface java.util.concurrent.Future<Value>
Value get();
Value
get(long durée, TimeUnit unitéDeTemps);
Récupère le résultat, en bloquant jusqu'à ce qu'il soit disponible ou que le délai soit expiré. La deuxième méthode déclenche une TimeoutException en cas d'échec.
boolean cancel(Boolean peutÊtreInterrompu);
Tente d'annuler l'exécution de cette tâche. Si la tâche a déjà commencé et que le paramètre peutÊtreInterrompu vaut true, elle est interrompue. Renvoie true si l'annulation a réussi.
boolean isCancelled();
Renvoie true si la tâche a été annulée avant la fin.
boolean isDone();
Renvoie true si la tâche est terminée, suite à une fin normale, une annulation ou une exception.
La classe java.util.concurrent.FutureTask<Value>
FutureTask<Value>(Callable<Value> tâche)
FutureTask<Value>(Runnable tâche, Value résultat)
Construit un objet qui est à la fois un Future<Value> et un Runnable.

Exemple de mise en oeuvre

Le programme ci-dessous illustre ces concepts. Ce programme est identique à l'exemple du chapitre précédent qui trouvait les fichiers contenant un mot clé. Toutefois, nous allons maintenant simplement compter le nombre de fichiers correspondants.

  1. Nous avons donc une longue tâche qui produit une valeur entière ; un exemple de Callable<Integer> :
    class TâcheDeRecherche implements Callable<Integer> {
       public TâcheDeRecherche(File répertoire, String mot) { ...   }
    
       public Integer call() { ...  } // Renvoie le nombre de fichiers concordants
    }
  2. Nous construisons ensuite un objet FutureTask<Integer> depuis TâcheDeRecherche et l'utilisons pour démarrer un thread :
    TâcheDeRecherche rechercher = new TâcheDeRecherche(new File(répertoire), mot);
    FutureTask<Integer> question = new FutureTask<Integer>(rechercher);
    Thread tâche = new Thread(question);
    tâche.start();
    
  3. Enfin, nous affichons le résultat :
    System.out.println(question.get() + " fichiers trouvés");
    

    Bien entendu, l'appel à get() bloque jusqu'à ce que le résultat soit disponible.
    §

  4. Dans la méthode call(), nous utilisons le même mécanisme à plusieurs reprises. Pour chaque sous-répertoires, nous produisons un nouveau TâcheDeRecherche et lançons un thread pour chacun.
  5. Nous répartissons aussi les objets ArrayList<Future<Integer>>. A la fin, nous ajoutons tous les résultats :
    for (Future<Integer> résultat : résultats)  compteur += résultat.get();
    
  6. Chaque appel à get() bloque jusqu'à ce que le résultat soit disponible. Bien sûr les threads s'exécutent en parallèle, il y a donc de fortes chances pour que les résultats soient tous disponibles à peu près au même moment.
Codage correspondant
package synchronisation;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

public class PoolDeThreads {
   private static String répertoire = "D:/Developpez.com";

   public static void main(String[] args) {
      Scanner clavier = new Scanner(System.in);
      System.out.print("Mot à rechercher : ");
      String mot = clavier.nextLine();

      TâcheDeRecherche rechercher = new TâcheDeRecherche(new File(répertoire), mot);
      FutureTask<Integer> question = new FutureTask<Integer>(rechercher);
      Thread tâche = new Thread(question);
      tâche.start();
      try {
         System.out.println(question.get() + " fichiers trouvés");
      }
      catch (ExecutionException e) { e.printStackTrace(); }
      catch (InterruptedException e) { }
   }
}

class TâcheDeRecherche implements Callable<Integer> {
   private File répertoire;
   private String mot;
   private int compteur;
   
   public TâcheDeRecherche(File répertoire, String mot) {
      this.répertoire = répertoire;
      this.mot = mot;
   }

   public Integer call() {
      compteur = 0;
      try {
         File[] fichiers = répertoire.listFiles();
         ArrayList<Future<Integer>> résultats = new ArrayList<Future<Integer>>();

         for (File fichier : fichiers)
            if (fichier.isDirectory()) {
               TâcheDeRecherche rechercher = new TâcheDeRecherche(fichier, mot);
               FutureTask<Integer> question = new FutureTask<Integer>(rechercher);
               résultats.add(question);
               Thread tâche = new Thread(question);
               tâche.start();
            }
            else if (recherche(fichier)) compteur++;

         for (Future<Integer> résultat : résultats)
            try {
               compteur += résultat.get();
            }
            catch (ExecutionException e) { e.printStackTrace(); }
      }
      catch (InterruptedException e) { }
      return compteur;
   }

   public boolean recherche(File fichier) {
      try {
         Scanner lecture = new Scanner(new FileInputStream(fichier));
         boolean trouvé = false;
         while (!trouvé && lecture.hasNextLine()) {
            String ligne = lecture.nextLine();
            if (ligne.contains(mot)) trouvé = true;
         }
         lecture.close();
         return trouvé;
      }
      catch (IOException e) { return false; }
   }
}

 

Choix du chapitre Exécuteurs

La construction d'un nouveau thread est assez onéreuse car elle implique une interaction avec le système d'exploitation. Si votre programme crée un grand nombre de threads courts, il devra plutôt utiliser un pool de threads.

Ce pool contient plusieurs threads inactifs, prêts à s'exécuter. Vous attribuez un Runnable au pool et l'un des threads appelle la méthode run(). A la fin de la méthode run(), le thread ne meurt pas, il demeure pour répondre à la requête suivante.

Il existe une autre raison pour utiliser un pool de threads : l'étranglement de plusieurs threads simultanés. La création d'un grand nombre de threads peut considérablement dégrader les performances, voire bloquer la machine virtuelle. Pour un algorithme créateur de nombreux threads, utilisez un pool de threads "fixe" qui limite le nombre total de threads simultanés.

La classe Executors

La classe Executors possède plusieurs méthodes de fabrique statiques destinées à la création de pools de threads :

Méthode de fabrique Description
newCachedThreadPool() Les nouveaux threads sont créés en fonction des besoins. Les threads inactifs sont conservés pendant 60 secondes.
newFixedThreadPool() Le pool contient un jeu fixe de threads. Les threads inactifs sont conservés indéfiniment.
newSingleThreadExecutor() Un "pool" possédant un seul thread qui exécute les tâches envoyées de manière séquentielle (comme le thread de répartition d'événements Swing).
newSheduledThreadPool() Un pool de threads fixe pour une exécution programmée, remplace java.util.Timer.
newSingleThreadScheduledExecutor() Un "pool" d'un seul thread pour une exécution programmée.

Pools de threads

Evaluons les premières méthodes, nous verrons les autres par la suite :

  1. La méthode newCachedThreadPool() construit un pool de threads qui exécute chaque tâche immédiatement, à l'aide d'un thread inactif existant lorsqu'il est disponible ou en créant un nouveau thread dans le cas contraire.
  2. La méthode newFixedThreadPool() construit un pool de threads de taille fixe. Si le nombre de tâches envoyées est supérieur au nombre de thread inactifs, les tâches non servies sont placées dans une queue. Elles sont exécutées à la fin des autres tâches.
  3. newSingleThreadExecutor() est un pool dégénéré, de taille 1 : un seul thread exécute les tâches envoyées, l'une après l'autre.

Ces trois méthodes renvoient un objet de la classe ThreadPoolExecutor qui implémente l'interface ExecutorService.
§

L'interface ExecutorService

Vous pouvez envoyer un Runnable ou un Callable à un ExecutorService à l'aide de l'une de ses méthodes submit() :

Future<?> submit(Runnable tâche)
Future<T> submit(Runnable tâche, T résultat)
Future<T> submit(Callable<T> tâche)

Le pool exécutera la tâche envoyée le plus tôt possible. Lorsque vous appelez la méthode submit(), vous recevez un objet Future disponible pour enquêter sur l'état de la tâche.

  1. La première méthode submit() renvoie un Future<?> à l'aspect étrange. Vous pouvez utiliser cet objet pour appeler isDone(), cancel() ou isCancelled(). Or la méthode get() renvoie simplement null à la fin.
  2. La deuxième version de submit() envoie aussi un Runnable et la méthode get() de Future renvoie l'objet résultat donné à la fin de l'opération.
  3. La troisième version envoie un Callable. Le Future renvoyé obtient le résultat du calcul lorsqu'il est prêt.

Lorsque vous avez terminé avec un pool de connexion, appelez la méthode shutdown(). Cette méthode lance la séquence de fermeture du pool. Un exécuteur fermé n'accepte plus de nouvelles tâches. Lorsque toutes les tâches sont terminées, les threads du pool meurent. Vous pouvez aussi appeler la méthode shutdownNow(). Le pool annule alors toutes les tâches qui n'ont pas encore commencé et tente d'interrompre les threads en cours d'exécution.

Séquence de mise en oeuvre d'un pool de thread

Voici, en bref, ce qu'il faut faire pour utiliser un pool de connexion :

  1. Appelez la méthode statique newCachedThreadPool() ou newFixedThreadPool() de la classe Executors.
  2. Appelez ensuite la méthode submit(), à partir de l'instance créée par l'une des méthodes précédentes, pour envoyer des objets Runnable ou Callable.
  3. Pour pouvoir annuler une tâche ou si vous envoyez des objets Callable, restez sur les objets Future renvoyés.
  4. Appelez la méthode shutdown() lorsque vous ne voulez plus envoyer de tâches.
La classe java.util.concurrent.Executors
ExecutorService newCachedThreadPool()
Renvoie un pool de thread en cache qui crée des threads selon les besoins et met fin aux threads inactifs depuis 60 secondes.
ExecutorService newFixedThreadPool()
Renvoie un pool de thread qui utilise le nombre donné de threads pour exécuter les tâches.
ExecutorService newSingleThreadExécutor()
Renvoie un Executor qui exécute les tâches de manière séquentielle dans un seul thread.
La classe java.util.concurrent.ExecutorService
Future<T> submit(Callable<T> tâche)
Future<T> submit(Runnable tâche, T résultat)
Future<?> submit(Runnable tâche)
Envoie la tâche donnée pour exécution.
void shutdown()
Arrête le sevice, en terminant les tâches déjà envoyées mais sans accepter les nouveaux envois.
La classe java.util.concurrent.ThreadPoolExecutor
int getLargestPoolSize()
Renvoie la plus grande taille du pool de threads pendant la vie de cet Executor.

Exemple de mise en oeuvre

L'exemple précédent produisait un grand nombre de threads courts, un par répertoire. Nous allons reprendre cet exemple en prenant en compte cette fois-ci un pool de threads pour lancer ces tâches. Vous remarquerez que la réactivité s'en trouve largement améliorée.

Le programme affiche, à la fin, la taille du pool, la plus grande pendant son exécution. Ces informations ne sont pas directement disponibles dans l'interface ExecutorService. Pour cette raison, nous devons transtyper l'objet pool sur la classe ThreadPoolExecutor.

run:
Mot à rechercher : tâche
6 fichiers trouvés
Taille du pool = 2147483647

BUILD SUCCESSFUL (total time: 25 seconds)


Codage correspondant
package synchronisation;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

public class RetourValeur {
   private static String répertoire = "D:/Developpez.com";

   public static void main(String[] args) {
      Scanner clavier = new Scanner(System.in);
      System.out.print("Mot à rechercher : ");
      String mot = clavier.nextLine();

      ExecutorService pool = Executors.newCachedThreadPool();
      TâcheDeRecherche rechercher = new TâcheDeRecherche(new File(répertoire), mot, pool);
      Future<Integer> résultat = pool.submit(rechercher);

      try {
         System.out.println(résultat.get() + " fichiers trouvés");
      }
      catch (ExecutionException e) { e.printStackTrace(); }
      catch (InterruptedException e) { }
      pool.shutdown();
      int taille = ((ThreadPoolExecutor)pool).getMaximumPoolSize();
      System.out.println("Taille du pool = "+taille);
   }
}

class TâcheDeRecherche implements Callable<Integer> {
   private File répertoire;
   private String mot;
   private int compteur;
   private ExecutorService pool;
   
   public TâcheDeRecherche(File répertoire, String mot, ExecutorService pool) {
      this.répertoire = répertoire;
      this.mot = mot;
      this.pool = pool;
   }

   public Integer call() {
      compteur = 0;
      try {
         File[] fichiers = répertoire.listFiles();
         ArrayList<Future<Integer>> résultats = new ArrayList<Future<Integer>>();

         for (File fichier : fichiers)
            if (fichier.isDirectory()) {
               TâcheDeRecherche rechercher = new TâcheDeRecherche(fichier, mot, pool);
               Future<Integer> résultat = pool.submit(rechercher);
               résultats.add(résultat);
            }
            else if (recherche(fichier)) compteur++;

         for (Future<Integer> résultat : résultats)
            try {
               compteur += résultat.get();
            }
            catch (ExecutionException e) { e.printStackTrace(); }
      }
      catch (InterruptedException e) { }
      return compteur;
   }

   public boolean recherche(File fichier) {
      try {
         Scanner lecture = new Scanner(new FileInputStream(fichier));
         boolean trouvé = false;
         while (!trouvé && lecture.hasNextLine()) {
            String ligne = lecture.nextLine();
            if (ligne.contains(mot)) trouvé = true;
         }
         lecture.close();
         return trouvé;
      }
      catch (IOException e) { return false; }
   }
}

Contrôle de groupes de tâche

Vous avez vu comment utiliser un service exécuteur sous forme de pool de threads pour augmenter l'efficacité de l'exécution de la tâche. Un exécuteur sert parfois à des fins plus tactiques, simplement pour contrôler un groupe de tâches liées.

  1. Ainsi, vous pouvez annuler toutes les tâches dans un exécuteur avec la méthode shutdownNow().
  2. La méthode invokeAny() envoie tous les objets d'une collection d'objets Callable et renvoie le résultat d'une tâche terminée. Vous ne connaissez pas la tâche (c'était probablement celle qui s'est terminée le plus rapidement). Vous utiliseriez cette méthode pour un problème de recherche dans lequel vous êtes prêt à accepter n'importe qu'elle solution.

    Ainsi, supposons que vous deviez factoriser un grand entier (un calcul requis pour casser un code RSA). Vous pourriez envoyer plusieurs tâches, chacune tentant une factorisation à l'aide de nombres d'une plage différente. Dès que l'une de ces tâches a une réponse, votre calcul peut s'arrêter.

  3. La méthode invokeAll() envoie tous les objets d'une collection d'objets Callable et renvoie une liste d'objets Future qui représentent les solutions à toutes les tâches. Vous pouvez traiter les résultats du calcul dès qu'ils sont disponibles, comme ceci :
    List<Callable<T>> tâches = ... ;
    List<Future<T>> résultats = exécuteur.invokeAll(tâches);
    for (Future<T> résultat : résultats)  traitement(résultat.get());

    L'inconvénient de cette méthode, c'est que vous risquez d'attendre sans raison valable si la première tâche prend du temps. Il serait plus logique d'obtenir les résultats dans l'ordre de leur disponibilité. Cela peut être arrangé avec ExecutorCompletionService.

    Commencer par un exécuteur, obtenu de la manière ordinaire. Construisez ensuite un ExecutorCompletionService. Envoyer les tâches au service de réalisation. Le service gère une queue de blocage d'objets Future, contenant les résultats des tâches soumises lorqu'elles deviennent disponibles.

    Une organisation plus efficace pour le calcul précédent correspond à ceci :
    List<Callable<T>> tâches = ... ;
    ExecutorCompletionService
    service = new ExecutorCompletionService(exécuteur); for (Callable<T> tâche : tâches) service.submit(tâche);
    for (int i=0; i<tâches.size(); i++) traitement(service.take().get());
La classe java.util.concurrent.ExecutorService
T invokeAny(Collection<Callable<T>> tâches)
T invokeAny(Collection<Callable<T>> tâches, long durée, TimeUnit unitéDeTemps)
Exécutent les tâches données et renvoient le résultat de l'une d'elles. La seconde méthode déclenche une TimeoutException en cas de temporisation dépassée.
List<Future<T>> invokeAll(Collection<Callable<T>> tâches)
List<Future<T>> invokeAll(Collection<Callable<T>> tâches, long durée, TimeUnit unitéDeTemps)
Exécutent les tâches données et renvoient le résultat de toutes. La seconde méthode déclenche une TimeoutException en cas de temporisation dépassée.
La classe java.util.concurrent.ExecutorCompletionService
ExecutorCompletionService(Executor exécuteur)
Construit une réalisation d'exécuteur qui collecte des résultats de l'exécuteur donné.
Future<T> submit(Callable<T> tâche)
Future<T> submit(Runnable tâche, T résultat)
Envoie une tâche à l'exécuteur sous-jacent.
Future<T> take()
Supprime le résultat terminé suivant, blocage au cas où aucun résultat terminé n'est disponible.
Future<T> poll()
Future<T> poll(long durée, TimeUnit unitéDeTemps)
Supprime le résultat terminé suivant ou null si aucun élément n'est disponible. La seconde méthode attend le délai indiqué.

Autre exemple de mise en oeuvre d'exécuteur - Pool de threads de plusieurs balles rebondissantes

Lorsque nous devons gérer beaucoup de petites tâches élémentaires, nous l'avons dit, il est beaucoup plus avantageux en termes de ressource et de performance, de prendre de pool de threads que nous avons nommé exécuteur. L'exemple, par excellence qui si prête bien, est notre petite application sur les balles rebondissantes.

La création du pool de thread se fait lorsque nous cliquons sur le bouton "Démarrer". A chaque fois que nous cliquons sur ce bouton, une nouvelle balle est créée dans son propre thread, et elle est rajoutée dans l'exécuteur. Si par contre vous cliquez sur le bouton "Arrêter", le pool de thread s'arrête définitivement, et du même coup, interrompt tous les threads. Lorsque de nouveau, vous cliquez sur le bouton "Démarrer", un nouvel exécuteur est créé.


Codage correspondant
package threads;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.concurrent.*;
import javax.swing.*;

public class PlusieursBalles extends JFrame {
   private JButton démarrer = new JButton("Démarrer");
   private JButton arrêter = new JButton("Arrêter");
   private JPanel boutons = new JPanel();
   private Panneau panneau = new Panneau();

   public PlusieursBalles() {
      super("Rebond de plusieurs balles");
      panneau.setBackground(Color.ORANGE);
      add(panneau);
      add(boutons, BorderLayout.SOUTH);
      boutons.add(démarrer);
      boutons.add(arrêter);
      démarrer.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {             
             panneau.ajoutBalle();
         }
      });
      arrêter.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            panneau.arrêtBalles();
         }
      });
      setSize(500, 400);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private class Panneau extends JPanel {
      private ExecutorService exécuteur;
      private ArrayList<Balle> balles = new ArrayList<Balle>();
      
      public void ajoutBalle() {
         if (exécuteur == null) exécuteur = Executors.newCachedThreadPool();
         Balle balle = new Balle();
         balles.add(balle);
         exécuteur.submit(balle);
      }
      
      public void arrêtBalles() {
         if (exécuteur != null) {
            exécuteur.shutdownNow();
            exécuteur = null;
            balles.clear();
         }
      }
      
      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         for (Balle balle : balles) surface.fill(balle.getForme());
      }
   }

   private class Balle implements Runnable {
      private double x, y, dx=5, dy=5;

      public void déplace(Rectangle2D zone) {
         x+=dx;
         y+=dy;
         if (x < zone.getMinX()) { x = zone.getMinX();  dx = -dx; }
         if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15;  dx = -dx; }
         if (y < zone.getMinY()) { y = zone.getMinY();  dy = -dy; }
         if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15;  dy = -dy; }
      }
      
      public Ellipse2D getForme() {
         return new Ellipse2D.Double(x, y, 15, 15);
      }
      public void run() {
         try {
            while (true) {
               déplace(panneau.getBounds());
               panneau.repaint();
               Thread.sleep(5);
            }
         }
         catch (InterruptedException ex) {  }
      }
   }

   public static void main(String[] args) { new PlusieursBalles(); }
}

 

Choix du chapitre Synchroniseurs

Le paquetage java.util.concurrent contient plusieurs classes qui permettent de gérer un jeu de threads de collaboration. Ces mécanismes possèdent des fonctionnalités intégrées pour les motifs communs de rencontre des threads.

Si vous disposez d'un jeu de threads de collaboration qui suit l'un de ces motifs de comportement, réutilisez simplement la classe de bibliothèque appropriée au lieu de concocter une collection artisanale de verrous et de conditions.

Classe Action Quand l'utiliser
CyclicBarrier Permet à un jeu de threads de patienter jusqu'à ce qu'un nombre prédéfini d'entre eux ait atteint une barrière commune, puis exécute, en option, une action de barrière. Lorsque plusieurs threads doivent se terminer avant que leurs résultats ne puissent être utilisées.
CountDownLatch Permet à un jeu de threads de patienter jusqu'à ce qu'un compteur ait été ramené à 0. Lorsqu'un ou plusieurs threads doivent patienter jusqu'à ce qu'un nombre spécifié de résultats soit disponibles.
Exchanger Permet à deux threads d'échanger des objets lorsque les deux sont prêts pour l'échange. Lorsque deux threads agissent sur deux instances de la même structure de données, l'un en remplissant une instance, l'autre en vidant l'autre instance.
SynchronousQueue Permet à un thread de donner un objet à un autre thread. Pour envoyer un objet d'un thread à un autre lorsque les deux sont prêts, sans synchronisation explicite.
Semaphore Permet à un jeu de threads de patienter jusqu'à ce que les autorisations de poursuivre soient disponibles. Pour limiter le nombre total de threads ayant accès à une ressource. Si le compte des autorisations est à un, à utiliser pour bloquer les threads jusqu'à ce qu'un autre thread donne une autorisation.

Sémaphores

Un sémaphore est un élément qui gère plusieurs autorisations.

  1. Pour passer au-delà du sémaphore, un thread demande une autorisation en appelant la méthode acquire(). Seul un nombre fixe d'autorisations est disponible, ce qui limite le nombre de threads autorisés à passer.
  2. D'autres threads peuvent émettre des autorisations en appelant la méthode release().

Mais, il ne s'agit pas de vrais objets d'autorisation. Le sémaphore conserve simplement un compteur. De plus, une autorisation n'a pas à être libérée par le thread qui l'acquiert. En fait, n'importe quel thread peut émettre n'importe quel nombre d'autorisation. S'il en émet plus que le maximum disponible, le sémaphore est simplement défini sur le compte maximal. Ils sont donc flexibles, mais peuvent paraître confus.

Les sémaphores ont été inventés par Edsger Dijkstra en 1968 pour être utilisés sous la forme de primitives de synchronisation. Mr Dijkstra a montré que les sémaphores pouvaient être efficacement implémentés et qu'ils sont suffisamment performants pour résoudre les problèmes communs de synchronisations des threads. Dans presque tous les systèmes d'exploitation, vous trouverez des implémentations de queues bornées utilisant des sémaphores.

Bien entendu, les programmeurs d'application n'ont pas à réinventer les queues bornées. Nous vous suggérons de n'utiliser les sémaphores que lorsque leur comportement concorde bien avec votre problème de synchronisations.

Par exemple, un sémaphore ayant un nombre d'autorisations égal à 1 servira de porte qu'un autre thread pourra ouvrir ou fermer. Vous verrez un peu plus loin un exemple dans lequel un thread travailleur produit une animation. Parfois, le thread travailleur attend qu'un utilisateur appuie sur un bouton. Le thread travailleur tente d'acquérir une autorisation, puis attend qu'un clic de bouton autorise l'émission d'une autorisation.

Verrous de décomptage

Un CountDownLatch permet à un jeu de threads de patienter jusqu'à ce qu'un compteur ait atteint zéro. Le CountDownLatch ne sert qu'une fois. Lorsqu'un compteur a atteint zéro, vous ne pouvez pas le réutiliser.

Il existe un cas spécial, où le compteur du verrou est à 1. Ceci implémente une porte d'utilisation unique. Les threads sont maintenus à la porte jusqu'à ce qu'un autre thread définissent le compte à 0.

Imaginons, par exemple, un jeu de threads qui ait besoin de données initiales pour effectuer son travail. Les threads travailleurs démarrent, puis patientent à la porte. Un autre thread prépare les données. Lorsqu'il est prêt, il appelle la méthode countDown() et tous les threads travailleurs poursuivent.

Vous pouvez utiliser un second verrou pour vérifier lorsque tous les threads travailleurs sont effectués. Initialisez le verrou avec le nombre de threads. Chaque thread travaileur décompte ce verrou juste avant de terminer. Un autre thread qui récolte les résultats du travail patiente sur le verrou et continue dès que tous les travailleurs ont terminés.

Barrières

La classe CyclicBarrier implémente ce que l'on appelle une barrière. Imaginons que plusieurs threads travaillent sur certaines parties d'un calcul. Lorsque toutes les parties sont prêtes, il faut combiner les résultats. Ainsi, lorsqu'un thread a terminé avec une partie, nous le laissons s'exécuter contre la barrière. Lorsque tous les threads ont atteint la barrière, celle-ci cède et les threads peuvent poursuivre.

  1. Pour cela, vous construisez d'abord une barrière, en indiquant le nombre de threads participants :
    CyclicBarrier barrière = new CyclicBarrier(nombreDeThreads);
  2. Chaque thread effectue un certain travail et appelle la méthode await() sur la barrière lorsqu'il a terminé :
    public void run() {
       faireTravail();
       barrière.await();
    }
  3. La méthode await() prend un paramètre optionnel de temporisation :
    barrière.await(100, TimeUnit.MILLISECONDS);
    
  4. Si l'un des threads attendant la barrière part, celle-ci se rompt (un thread peut partir s'il appelle await() avec une temporisation ou parce qu'il a été interrompu). Dans ce cas, la méthode await() de tous les autres threads déclenche une BrokenBarrierException. Les threads qui attendent déjà voient leur méthode await() se terminer immédiatement.
Vous pouvez fournir une action de barrière optionnelle, qui s'exécutera lorsque tous les threads auront atteint la barrière. L'action peut récupérer le résultat de chaque thread.
Runnable actionBarrière = ...;
CyclicBarrier
barrière = new CyclicBarrier(nombreDeThreads, actionBarrière);

La barrière est dite cyclique car elle peut être réutilisée après libération de tous les threads en attente. Elle diffère en cela d'un CountDownLatch, qui ne peut être utilisé qu'une seule fois.

Echangeur

Exchanger est utilisé lorsque deux threads travaillent sur deux instances du même tampon de données. Généralement, un thread remplit le tampon, tandis que l'autre consomme son contenu. Lorsque les deux ont terminé, ils échangent leurs tampons.

Queues synchrones

Une queue synchrone est un mécanisme qui apparie un thread producteur et un thread consommateur. Lorsqu'un thread appelle la méthode put() sur un SynchronousQueue, il bloque jusqu'à ce qu'un autre thread appelle la méthode take(), et vice versa. Â la différence d'Exchanger, les données ne sont transférées que dans une direction, du producteur au consommateur.

La classe java.util.concurrent.CyclicBarrier
CyclicBarrier(int parties)
CyclicBarrier(int parties, Runnable tâche)
Construisent une barrière cyclique pour le nombre de parties donné. tâche est exécuté lorsque toutes les parties ont appelé await() sur la barrière.
int await()
int await(long durée, TimeUnit unitéDeTemps)
Attendent que toutes les parties aient appelé await() sur la barrière ou jusqu'à la fin de la temporisation, auquel cas une TimeoutException est déclenchée. En cas de réussite, renvoient l'indice d'arrivée de cette partie. La première partie possède parties -1 indices, la dernière partie possède un indice 0.
La classe java.util.concurrent.CountDownLatch
CountDownLatch(int compteur)
Construit un verrou avec un compte à rebours donné.
void await()
Attend que le compteur de ce verrou est atteint 0.
boolean await(long durée, TimeUnit unitéDeTemps)
Attend que le compteur de ce verrou est atteint 0 ou que la temporisation ait expiré. Renvoie true si le compteur vaut 0, false si la temporisation a expiré.
void countDown()
Décompte le compte à rebour de ce verrou.
La classe java.util.concurrent.Exchanger<Value>
Value exchange(Value élément)
Value
exchange(Value élément, long durée, TimeUnit unitéDeTemps)
Bloquent jusqu'à ce qu'un autre thread appelle cette méthode, puis échange l'élément avec l'autre thread et renvoient l'élément de l'autre thread. La deuxième méthode déclenche une TimeoutException après expiration de la temporisation.
La classe java.util.concurrent.SynchronousQueue<Value>
SynchronousQueue()
SynchronousQueue(boolean favoriser)
Construisent une queue synchrone qui permet aux threads de donner des éléments. Si favoriser vaut true, la queue favorise les threads attendant depuis le plus longtemps.
void put(Value élément)
Bloque jusqu'à ce qu'un autre thread appelle take() pour prendre cet élément.
Value take()
Bloque jusqu'à ce qu'un autre thread appelle put(). Renvoie l'élément fourni par l'autre thread.
La classe java.util.concurrent.Semaphore
Semaphore(int authorisations)
Semaphore(int authorisations, boolean favoriser)
Construisent un sémaphore avec le nombre maximal donné d'autorisations. Si favoriser vaut true, la queue favorise les threads attendant depuis le plus longtemps.
void acquire()
Attend d'acquérir une authorisation.
boolean tryAcquire()
Tente d'acquérir une autorisation ; renvoie false si aucune n'est disponible.
boolean tryAcquire(long durée, TimeUnit unitéDeTemps)
Tente d'acquérir une autorisation avec le délai donné ; renvoie false si aucune n'est disponible.
void release()
void
release(int authorisations)
Libère une ou plusieurs autorisations suivant la valeur spécifiée.

Exemple : pause et reprise d'une animation

Nous allons reprendre notre application sur les balles rebondissantes. Cette fois-ci, nous contrôlons parfaitement l'animation des balles déjà présentes. Il est possible maintenant de faire une pause et ainsi de figer l'ensemble des balles et de les relancer par la suite lorsque nous le désirons.

Un sémaphore avec un compteur d'autorisation de 1 peut servir à synchroniser l'ensemble des threads représentant le mouvement des balles. Si nous demandons de mettre en pause avec le bouton "Pause", chaque thread de l'animation appelle la méthode acquire(). Quand plus tard l'utilisateur clique sur le bouton "Relancer", le thread d'interface utilisateur appelle la méthode release(), en spécifiant le nombre de threads à libérer (correspondant au nombre de balles déjà présentes). Ainsi toutes les balles redeviennent animées.

Codage correspondant
package threads;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.util.ArrayList;
import java.util.concurrent.*;
import javax.swing.*;

public class PlusieursBalles extends JFrame {
   private JToolBar barre = new JToolBar();
   private Panneau panneau = new Panneau();
   private Semaphore barrière = new Semaphore(1);
   private boolean animation = false;

   public PlusieursBalles() {
      super("Rebond de plusieurs balles");
      panneau.setBackground(Color.GREEN);
      add(panneau);
      add(barre, BorderLayout.SOUTH);
      barre.setLayout(new FlowLayout());
      barre.add(new AbstractAction("Une balle de plus") {
         public void actionPerformed(ActionEvent e) {
            panneau.ajoutBalle();
         }
      });
      barre.add(new AbstractAction("Pause") {
         public void actionPerformed(ActionEvent e) {
            panneau.pause();
         }
      });
      barre.add(new AbstractAction("Relancer") {
         public void actionPerformed(ActionEvent e) {
            panneau.relancer();
         }
      });
      barre.add(new AbstractAction("Tout recommencer") {
         public void actionPerformed(ActionEvent e) {
            panneau.toutRecommencer();
         }
      });
      setSize(500, 400);
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private class Panneau extends JPanel {
      private ExecutorService exécuteur;
      private ArrayList<Balle> balles = new ArrayList<Balle>();
      
      public void ajoutBalle() {
         if (exécuteur == null) exécuteur = Executors.newCachedThreadPool();
         Balle balle = new Balle();
         balles.add(balle);         
         exécuteur.submit(balle);
         if (!animation) relancer();
      }
      
      public void pause() {
         animation = false;
      }
      
      public void relancer() {
         animation = true;
         barrière.release(balles.size());
      }

      public void toutRecommencer() {
         if (exécuteur != null) {
            exécuteur.shutdownNow();
            exécuteur = null;
            balles.clear();
            ajoutBalle();
         }
      }
      
      @Override
      protected void paintComponent(Graphics g) {
         super.paintComponent(g);
         Graphics2D surface = (Graphics2D) g;
         for (Balle balle : balles) surface.fill(balle.getForme());
      }
   }

   private class Balle implements Runnable {
      private double x, y, dx=5, dy=5;

      public void déplace(Rectangle2D zone) {
         x+=dx;
         y+=dy;
         if (x < zone.getMinX()) { x = zone.getMinX();  dx = -dx; }
         if (x+15 >= zone.getMaxX()) { x = zone.getMaxX() - 15;  dx = -dx; }
         if (y < zone.getMinY()) { y = zone.getMinY();  dy = -dy; }
         if (y+15 >= zone.getMaxY()) { y = zone.getMaxY() - 15;  dy = -dy; }
      }
      
      public Ellipse2D getForme() {
         return new Ellipse2D.Double(x, y, 15, 15);
      }
      public void run() {
         try {
            while (true) {
               if (animation) {
                  déplace(panneau.getBounds());
                  panneau.repaint();
                  Thread.sleep(5);
               }
               else barrière.acquire();
            }
         }
         catch (InterruptedException ex) {  }
      }
   }

   public static void main(String[] args) { new PlusieursBalles(); }
}

 

Choix du chapitre Les threads avec Swing

Nous l'avons vu dans l'introduction de cette étude, l'une des raisons poussant à utiliser les threads dans vos programmes est que ces derniers répondront mieux. Losqu'un programme doit réaliser une tâche de longue durée, vous pouvez lancer un autre travailleur au lieu de bloquer l'interface utilisateur.

Attention toutefois à ce que vous faites dans un thread travailleur car, et cela peut surprendre, Swing n'est pas compatible avec les threads. Si vous tentez de manipuler des éléments graphiques à partir de plusieurs threads, l'interface utilisateur peut s'endommager.

Evaluation du problème

Pour constater ce problème, exécutez le programme suivant. Lorsque vous cliquez sur le bouton "Mauvais", un nouveau thread est démarré. Sa méthode run() torture une liste déroulante, en ajoutant et supprimant des valeurs de manière aléatoire

Codage correspondant
import java.awt.FlowLayout;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

class ThreadSwing extends JFrame {
   private JComboBox combo = new JComboBox();
   private JButton mauvaisBouton = new JButton("Mauvais");
   
   public ThreadSwing() {
      setTitle("Test entre Swing et les threads");      
      combo.insertItemAt(Integer.MAX_VALUE, 0);
      combo.setPrototypeDisplayValue(combo.getItemAt(0));
      combo.setSelectedIndex(0);      
      mauvaisBouton.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent event) {
            new Thread(new MauvaisThread(combo)).start();
         }
      });
      add(mauvaisBouton);
      add(combo);
      setLayout(new FlowLayout());
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] a) { new ThreadSwing(); }
}

class MauvaisThread implements Runnable {
   private JComboBox combo;
   private Random générateur;

   public MauvaisThread(JComboBox aCombo) {
      combo = aCombo;
      générateur = new Random();
   }

   public void run() {
      try {
         while (true) {
            int i = Math.abs(générateur.nextInt());
            if (i % 2 == 0) combo.insertItemAt(i, 0);
            else if (combo.getItemCount() > 0)
            combo.removeItemAt(i % combo.getItemCount());
            Thread.sleep(1);
         }
      }
      catch (InterruptedException e) { }
   }
}

Testez-le. Cliquez sur le bouton "Mauvais", puis cliquez plusieurs fois sur la zone déroulante. Déplacez la barre de défilement. Déplacez la fenêtre. Cliquez à nouveau sur le bouton "Mauvais". Continuez à cliquer sur la zone déroulante. Enfin, vous devez avoir un rapport d'exception.

Que se passe-t-il ? Lorsqu'un élément est inséré dans la zone déroulante, celle-ci déclenche un événement pour actualiser l'affichage. Le code d'affichage se met ensuite en route, il lit la taille actuelle de la zone déroulante, puis se prépare à afficher les valeurs. Mais le thread travailleur continue à fonctionner, entraînant parfois une réduction du nombre des valeurs dans la zone déroulante. Le code d'affichage pense alors qu'il y a plus de valeurs dans le modèle que ce qu'il y en a réellement. Il demande des valeurs inexistantes, puis déclenche une exception ArrayIndexOutOfBounds.

Cette situation aurait pu être évitée en permettant aux programmeurs de vérouiller l'objet zone déroulante pendant son affichage. Or, les concepteurs de Swing ont décidé de ne faire aucun effort pour rendre Swing compatible avec les threads, et ce pour deux raisons :
  1. Tout d'abord, la synchronisation prend du temps et personne ne voulait ralentir Swing encore plus.
  2. Et surtout, l'équipe Swing a étudié l'expérience que d'autres équipes avaient mises en place avec les boîtes à outils d'interfaces utilisateurs compatibles avec les threads. Les constatations étaient peu encourageantes. Les programmeurs utilisant des boîtes à outils compatibles avec les threads ont été déroutés par les demandes de synchronisation et ont souvent créé des programmes sujets aux verrous morts.

Exécution de tâche longue

Lorsque vous utilisez des threads avec Swing, deux règles simples doivent être respectées :

  1. Si une action est trop longue, effectuez-la dans un thread travailleur séparé et jamais dans le thread de répartition des événements.

    La raison de cette première règle est simple à comprendre. Si vous passez trop de temps dans le thread de répartition d'événements, l'application semble "morte" car elle ne répond plus à aucun événement.

    Notamment, le thread de répartition d'événements ne doit jamais réaliser d'appels en entrée/sortie, qui pourraient se bloquer indéfiniment, et il ne doit jamais appeler la méthode sleep() (si vous devez attendre un délai spécifique, utilisez les événements de minuteur).

  2. Ne touchez pas aux composants Swing dans un thread autre que le thread de répartition des événements.

    La seconde règle est souvent appelée règle du thread unique pour la programmation Swing. Nous la découvrirons un peu plus loin.
    §

Ces deux règles semblent entrer en conflit. Supposons que vous déclenchiez un thread séparé pour qu'il exécute une tâche longue. Vous voulez généralement actualiser l'interface utilisateur pour signaler la progression lorsque votre thread travaille. Une fois la tâche terminée, vous devez actualiser à nouveau l'interface graphique. Vous ne pouvez toutefois pas toucher au composant Swing à partir de votre thread.

Par exemple, pour actualiser une barre de progression ou une étiquette de texte, vous ne pouvez pas simplement régler sa valeur à partir de votre thread.

Pour résoudre ce problème, il existe deux méthodes dans tous les threads pour ajouter des actions arbitraires à la queue des événements. Ainsi, supposons que vous vouliez régulièrement actualiser une étiquette dans un thread pour signaler une progression. Vous ne pouvez pas appeler la méthode setText() de l'étiquette à partir du thread. Utilisez plutôt les méthodes invokeLater() et invokeAndAwait() de la classe EventQueue pour que cet appel soit exécuté dans le thread de répartition des événements.

EventQueue.invokeLater(new Runnable() {
   public void run() {
       étiquette.setText("Progression : "+progression);
   }
});
  1. La méthode invokeLater() retourne immédiatement lorsque l'événement est envoyé dans la queue des événements. La méthode run() est exécutée de manière asynchrone.
  2. La méthode invokeAndWait() attend que la méthode run() ait réellement été exécutée.

Dans le cas d'une mise à jour d'une étiquette de progression, la méthode invokeLater() convient mieux. Les utilisateurs préfère que le thread travailleur continue à évoluer, plutôt que d'avoir un indicateur de progression plus précis.

Les deux méthodes exécutent la méthode run() dans le thread de répartition des événements. Aucun nouveau thread n'est créé.
§

Exemple de mise en oeuvre

Reprenons l'exemple précédent. Nous rajoutons un bouton dénommé "Bon". Ce nouveau code nous montre comment utiliser la méthode invokeLater() pour modifier en toute sécurité le contenu d'une zone déroulante.

Si vous cliquez sur le bouton "Bon", un thread insère et supprime des nombres. Toutefois, la modification réelle a lieu dans le thread de répartition des événements.


Codage correspondant
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;

class ThreadSwing extends JFrame {
   private JComboBox combo = new JComboBox();
   private JToolBar boutons = new JToolBar();

   public ThreadSwing() {
      setTitle("Swing et les threads");
      combo.insertItemAt(Integer.MAX_VALUE, 0);
      combo.setPrototypeDisplayValue(combo.getItemAt(0));
      combo.setSelectedIndex(0);
      boutons.add(new AbstractAction("Mauvais") {
         public void actionPerformed(ActionEvent e) {
            new Thread(new MauvaisThread(combo)).start();
         }
      });
      boutons.add(new AbstractAction("Bon") {
         public void actionPerformed(ActionEvent e) {
            new Thread(new BonThread(combo)).start();
         }
      });
      boutons.add(combo);
      add(boutons);
      setLayout(new FlowLayout());
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] a) { new ThreadSwing(); }
}

class MauvaisThread implements Runnable {
   private JComboBox combo;
   private Random générateur;

   public MauvaisThread(JComboBox unCombo) {
      combo = unCombo;
      générateur = new Random();
   }

   public void run() {
      try {
         while (true) {
            int i = Math.abs(générateur.nextInt());
            if (i % 2 == 0) combo.insertItemAt(i, 0);
            else if (combo.getItemCount() > 0)
               combo.removeItemAt(i % combo.getItemCount());
            Thread.sleep(1);
         }
      }
      catch (InterruptedException e) { }
   }
}

class BonThread implements Runnable {
   private JComboBox combo;
   private Random générateur;

   public BonThread(JComboBox unCombo) {
      combo = unCombo;
      générateur = new Random();
   }

   public void run() {
      while (true) {
         try {
            EventQueue.invokeLater(new Runnable() {
               public void run() {
                  int i = Math.abs(générateur.nextInt());
                  if (i % 2 == 0) combo.insertItemAt(i, 0);
                  else if (combo.getItemCount() > 0) 
                     combo.removeItemAt(i % combo.getItemCount());
               }
            });
            Thread.sleep(1);
         }
         catch (InterruptedException ex) { }
      }
   }
}
La classe java.awt.EventQueue
static void invokeLater(Runnable tâche)
Amène la méthode run() de l'objet tâche à s'exécuter dans le thread de répartition des événements après que les événements en attente aient été traités.
static void invokeAndWait(Runnable tâche)
Amène la méthode run() de l'objet tâche à s'exécuter dans le thread de répartition des événements après que les événements en attente aient été traités. Cet appel se bloque jusqu'à ce que la méthode run() se termine.
static boolean isDispatchThread()
Renvoie true si le thread exécutant cette méthode est le thread de répartition des événements.

Utilisation du travailleur Swing

Lorsqu'un utilisateur émet une commande pour laquelle le traitement est long, vous devez déclencher un nouveau thread pour qu'il effectue le travail. Nous venons de le voir, ce thread doit utiliser la méthode invokeLater() de la classe EventQueue pour actualiser l'interface utilisateur.

Il existe maintenant, à partir de Java SE 6.0, une classe vraiment intéressante qui simplifie encore plus le travail, et qui permet surtout de suivre une progression. Il s'agit de la classe SwingWorker.

Mise en oeuvre de la classe SwingWorker

Afin de bien maîtriser cette classe SwingWorker, nous allons mettre en oeuvre une petite application qui possède des commandes pour charger un fichier texte et annuler le traitement de chargement. Tester le programme avec un fichier relativement long.

  1. Le fichier est chargé dans un thread séparé.
  2. Lorsque le fichier se lit, le bouton "Ouvrir" est désactivé alors que le bouton "Annuler" devient actif.
  3. Après que chaque ligne a été lu, un compteur d'octets est mis à jour, ce qui permet d'évaluer le pourcentage de la progression de lecture qui apparaît à la fois dans la barre d'outils et sur la barre de progression.

  4. Une fois la procédure terminée, le bouton "Ouvrir" est réactivé, le bouton "Annuler" est estompé et le texte de la barre d'outil indique "Le fichier est téléchargé entièrement".
Cet exemple montre les activités d'interface utilisateur ordinaires d'une tâche en arrière-plan
  1. Après chaque unité de travail, actualiser l'interface utilisateur pour afficher la progression.
  2. Une fois le travail terminé, apporter une dernière modification à l'interface utilisateur.
La classe SwingWorker simplifie l'implémentation de cette tâche. Voici la procédure à suivre :
  1. Vous écrasez la méthode doInBackground() pour effectuer le travail de longue haleine et vous appelez parfois la méthode publish() pour communiquer la progression du travail. Cette méthode est exécutée dans un thread travailleur.
  2. La méthode publish() amène une méthode progress() à s'exécuter dans le thread de répartition des événements pour traiter les données de la progression.
  3. A la fin du travail, la méthode done() est appelée dans le thread de répartition des événements pour que vous finissiez la mise à jour de l'interface utilisateur.
  4. Dès que vous souhaitez travailler dans le thread travailleur, construisez un nouveau travailleur (chaque objet travailleur a pour but d'être utilisé une seule fois). Appelez ensuite la méthode execute(). Vous appelerez généralement la méthode execute() sur le thread de répartition des événements, mais ce n'est pas une obligation.
  5. Nous pouvons supposer qu'un travailleur produise un résultat d'un certain type (SwingWorker<Type, Value> implémente Future<Type>). Ce résultat peut être obtenu par la méthode get() de l'interface Future.
  6. Puisque la méthode get() se bloque jusqu'à ce que le résultat soit disponible, vous ne devez pas l'appeler immédiatement après execute(). Généralement, vous l'appelez depuis la méthode done() (il n'y a aucune obligation d'appeler get(), parfois, le traitement des données de progression suffit).
  7. Les données de progression intermédiaire et le résultat final peuvent avoir des types arbitraires. La classe SwingWorker possède ces types comme paramètres de type. Ainsi, un SwingWorker<Type, Value> produit un résultat de type Type et des données de type Value.
  8. Pour annuler l'opération en cours, utilisez la méthode cancel() de l'interface Future. Lorsque le travail est annulé, la méthode get() déclenche une CancellationException.
  9. Nous l'avons indiqué, l'appel du thread travailleur à publish() entraîne des appels à process() sur le thread de répartition des événements. Pour des raisons d'efficacité, les résultats de plusieurs appels à publish() peuvent être regroupés en un seul appel à process(). La méthode process() reçoit un List<Value> contenant tous les résultats intermédiaires.
Prise en compte de tous ces critères pour notre application
  1. Pour montrer à l'utilisateur qu'une progression est en cours, nous allons afficher le pourcentage du nombre d'octets lus dans la barre d'outils et sur la barre de progression. Ainsi, les données de progression sont composées du pourcentage d'octets déjà lus par rapport à la taille totale du fichier et de la ligne actuelle du texte. Nous les emballons dans une classe interne triviale :
    private class ProgressionDonnées {
       public int pourcentage;
       public String ligne;
    }
    
  2. Le résultat final est le texte qui a été lu dans un StringBuilder. Nous avons donc besoin d'un SwingWorker<StringBuilder, ProgressionDonnées>.
  3. Dans la méthode doInBackground(), nous lisons un fichier, ligne par ligne. Après chaque ligne, nous appelons la méthode publish() pour publier le pourcentage d'octets déjà introduits et le texte de la ligne actuelle.
    @Override
    public StringBuilder doInBackground() throws IOException, InterruptedException {
       int nombreOctets = 0;
       Scanner entrée = new Scanner(new FileInputStream(fichier));
       while (entrée.hasNextLine()) {
           String ligne = entrée.nextLine();
           nombreOctets += ligne.length()+2;
           texte.append(ligne);
           texte.append("\n");
           ProgressionDonnées données = new ProgressionDonnées();
           données.pourcentage = (int) (nombreOctets * 100 / taille);
           données.ligne = ligne;
           publish(données);
           Thread.sleep(1); // pour tester l'annulation ; inutile dans vos programmes
       }
      return texte;
    }
    

    Nous appelons également la méthode sleep() pendant un millième de seconde après chaque ligne pour que vous puissiez tester l'annulation sans être stressé, mais vous ne voulez pas ralentir vos programmes avec sleep(). Si vous commentez cette ligne, vous verrez que votre texte se charge très rapidement, avec seulement quelques mise à jour d'interface utilisateur par lot.

    Vous pouvez amener ce programme à se comporter de manière assez homogène en actualisant la zone de texte à partir du thread travailleur, mais ce n'est pas possible pour la plupart des composants Swing. Nous montrons ici l'approche générale dans laquelle les mises à jour des composants surviennent dans le thread de répartition des événements.

  4. Dans la méthode process(), nous ignorons tous les pourcentages intermédiaires à l'exception du dernier et concaténons toutes les lignes pour une mise à jour unique dans la zone de texte.
    @Override
    public void process(List<ProgressionDonnées> données) {
       if (isCancelled()) return;
       StringBuilder nouvelleLigne = new StringBuilder();
       int pourcentage = données.get(données.size() - 1).pourcentage;
       progression.setValue(pourcentage);
       commentaire.setText("Lecture du fichier en cours ... "+pourcentage+" %");
       for (ProgressionDonnées donnée : données) {
          nouvelleLigne.append(donnée.ligne);
          nouvelleLigne.append("\n");
       }
      zoneDeTexte.append(nouvelleLigne.toString());
    }
    
  5. Dans la méthode done(), la zone de texte est mise à jour avec la totalité du texte et le bouton "Annuler" est désactivé.
    @Override
    public void done() {
       try {
           StringBuilder résultat = get();
           zoneDeTexte.setText(résultat.toString());
           commentaire.setText("Le fichier est téléchargé entièrement");
       }
       catch (InterruptedException ex) { }
       catch (CancellationException ex) {
           zoneDeTexte.setText("");
           commentaire.setText("Annulation de la lecture de ce document");
       }
       catch (ExecutionException ex) {
           commentaire.setText("" + ex.getCause());
       }
       annuler.setEnabled(false);
       ouvrir.setEnabled(true);
    } 
  6. Remarquez que le travailleur est démarré dans l'écouteur d'événement pour le bouton "Ouvrir".
Cette technique simple permet d'exécuter des tâche longues tout en assurant l'attention de l'interface utilisateur.

La classe javax.swing.SwingWorker<Type, Value>
abstract Type doInBackground()
Ecrase cette méthode pour réaliser la tâche en arrière-plan et renvoyer le résultat du travail.
void process(List<Value> données)
Ecrase cette méthode pour traiter les données de progression intermédiaire dans le thread de répartition des événements.
void publish(Value ... donnée)
Transfère cles données de progression intermédiaire dans le thread de répartition des événements. Appelez cette méthode à partir de doInBackgound().
Codage final correspondant
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.*;

import javax.swing.*;

public class ThreadSwing extends JFrame {
   private JFileChooser sélection = new JFileChooser();;
   private JTextArea zoneDeTexte = new JTextArea();
   private JToolBar boutons = new JToolBar();
   private Action ouvrir;
   private Action annuler;
   private JProgressBar progression = new JProgressBar();
   private JLabel commentaire = new JLabel(" ");
   private SwingWorker<StringBuilder, ProgressionDonnées> texte;
   
   public ThreadSwing() {
      sélection.setCurrentDirectory(new File("."));
      add(new JScrollPane(zoneDeTexte));
      setSize(450, 350);
      add(progression, BorderLayout.SOUTH);
      boutons.add(ouvrir = new AbstractAction("Ouvrir") {
         public void actionPerformed(ActionEvent event) {
            int résultat = sélection.showOpenDialog(null);
            if (résultat == JFileChooser.APPROVE_OPTION) {
               zoneDeTexte.setText("");
               ouvrir.setEnabled(false);
               texte = new LectureTexte(sélection.getSelectedFile());
               texte.execute();
               annuler.setEnabled(true);
            }
         }
      });      
      boutons.add(annuler = new AbstractAction("Annuler") {
         public void actionPerformed(ActionEvent event) {
             texte.cancel(true);
         }
      });
      boutons.addSeparator();
      boutons.add(commentaire);
      add(boutons, BorderLayout.NORTH);
      annuler.setEnabled(false);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new ThreadSwing(); }

   private class ProgressionDonnées {
      public int pourcentage;
      public String ligne;
   }

   private class LectureTexte extends SwingWorker<StringBuilder, ProgressionDonnées> {
      private File fichier;
      private StringBuilder texte = new StringBuilder();
      private long taille;
      
      public LectureTexte(File fichier) {
         this.fichier = fichier;
         taille = fichier.length();
      }
      
      @Override
      public StringBuilder doInBackground() throws IOException, InterruptedException {
         int nombreOctets = 0;
         Scanner entrée = new Scanner(new FileInputStream(fichier));
         while (entrée.hasNextLine()) {
            String ligne = entrée.nextLine();
            nombreOctets += ligne.length()+2;
            texte.append(ligne);
            texte.append("\n");
            ProgressionDonnées données = new ProgressionDonnées();
            données.pourcentage = (int) (nombreOctets * 100 / taille);
            données.ligne = ligne;
            publish(données);
            Thread.sleep(1); // pour tester l'annulation ; inutile dans vos programmes
         }
         return texte;
      }

      @Override
      public void process(List<ProgressionDonnées> données) {
         if (isCancelled()) return;
         StringBuilder nouvelleLigne = new StringBuilder();
         int pourcentage = données.get(données.size() - 1).pourcentage;
         progression.setValue(pourcentage);
         commentaire.setText("Lecture du fichier en cours ... "+pourcentage+" %");
         for (ProgressionDonnées donnée : données) {
            nouvelleLigne.append(donnée.ligne);
            nouvelleLigne.append("\n");
         }
         zoneDeTexte.append(nouvelleLigne.toString());
      }

      @Override
      public void done() {
         try {
            StringBuilder résultat = get();
            zoneDeTexte.setText(résultat.toString());
            commentaire.setText("Le fichier est téléchargé entièrement");
         }
         catch (InterruptedException ex) { }
         catch (CancellationException ex) {
            zoneDeTexte.setText("");
            commentaire.setText("Annulation de la lecture de ce document");
         }
         catch (ExecutionException ex) {
            commentaire.setText("" + ex.getCause());
         }
         annuler.setEnabled(false);
         ouvrir.setEnabled(true);
      }
   }
}

La règle du thread unique

Chaque application Java démarre par une méthode main() qui s'exécute dans le thread principal. Dans un programme Swing, le thread principal est court. Il programme la construction de l'interface utilisateur dans le thread de répartition des événements puis s'arrête. Une fois l'interface utilisateur construite, le thread de répartition des événements traite les notifications des événements, comme les appels à actionPerformed() ou paintComponent(). D'autres threads, comme celui qui envoie les événements dans le queue des événements, s'exécutent en coulisse mais ils sont invisibles pour le programmeur.

Précédemment, au cours de ce chapitre, nous avons présenté la règle du thread unique : "Ne touchez pas aux composants Swing d'un thread autre que le thread de répartition des événements". Nous allons détailler cette règle.

Il existe quelques exceptions :

  1. Vous pouvez ajouter et supprimer en toute sécurité des écouteurs d'événements de n'importe quel thread. Bien entendu, les méthodes d'écouteur seront appelés dans le thread de répartition des événements.
  2. Un petit nombre de méthodes Swing sont compatibles avec les threads. Elles sont désignées dans la documentation API par la phrase "This method is thread safe, although most Swing methods are not" (cette méthode est compatible avec les threads, même si la plupart des méthodes Swing ne le sont pas). Les plus utiles sont :

Historiquement, la règle du thread unique était plus permissive. N'importe quel thread avait le droit de construire des composants, de définir leurs propriétés et de les ajouter dans des conteneurs, tant qu'aucun des composants n'avait été réalisé. Un composant est réalisé s'il peut recevoir des événements de dessin ou de validation. C'est la cas dès que les méthodes setVisible(true) ou pack() ont été appelées sur le composant ou si le composant a été ajouté à un conteneur qui a été réalisé.

Cette version de la règle du thread unique était commode. Elle nous permettait de créer l'interface graphique dans la méthode main(), puis d'appeler setVisible(true) sur le cadre de niveau supérieur de l'application. Il n'existait aucun planification gênante d'un Runnable sur le thread de répartition des événements.

Malheureusement, certains implémenteurs de composants ne font pas attention aux subtilités de la règle du thread unique. Ils lancent des activités sur le thread de répartition des événements sans jamais s'inquiéter de vérifier si le composant est réalisé. Ainsi, si vous appelez setSelectionStart() ou setSelectionEnd() sur un JTextComponent, un mouvement du curseur de texte est planifié dans le thread de répartition des événements, même si le composant n'est pas visible.

Il aurait pu être possible de détecter et de résoudre ces problèmes, mais les concepteurs de Swing ont choisi la solution de facilité. Ils ont décrété qu'il n'était jamais sûr d'accéder aux composants depuis n'importe quel thread de répartition des événements.

Vous devez donc construire l'interface utilisateur dans le thread de répartition des événements, à l'aide de l'appel à la méthode invokeLater() de la classe EventQueue :
class ThreadSwing extends JFrame {
...
   public ThreadSwing() {
...
   }

   public static void main(String[] a) throws Exception { 
       EventQueue.invokeLater(new Runnable() {
          public void run() {
             new ThreadSwing();               
          }
       });       
   }
}

Il existe bien entendu de nombreux programmes qui ne sont pas attentifs et concervent l'ancienne version de la règle du thread unique, en initialisant l'interface utilisateur sur le thread principal. Ces programmes portent le léger risque qu'une partie de l'initialisation de l'interface utilisateur entraîne des actions sur le thread de répartition des événements entrant en conflit avec les actions du thread principal.

Dernier exemple qui permet de transférer de gros fichiers sur le réseau

Afin de bien maîtriser le concept de thread avec les composants Swing, je vous propose de mettre en oeuvre un système client-serveur qui permet de transférer des gros fichiers sur le réseau et de contrôler ainsi la progression du transfert.

Deux programmes doivent donc être mise en place, d'une part le service qui stocke (uniquement, c'est un cas d'école) le fichier désiré dans un répertoire prédéfini, et d'autre part le client qui permet de choisir le fichier et de l'envoyer ensuite au service, sur le poste distant. Voici, la représentation de la partie cliente.

Vous pourrez rajouter par la suite des commandes supplémentaires, à la fois sur le client et le serveur, afin de pouvoir récupérer les fichiers stockés, de supprimer les fichiers déjà enregistrés, de lister les fichiers déjà présents, etc.

Codage correspondant côté client
package réseau;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.io.*;
import java.net.Socket;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.swing.*;

public class Client extends JFrame {
   private JFileChooser sélection = new JFileChooser();
   private JToolBar boutons = new JToolBar();
   private JTextField résultat = new JTextField("Stockage de fichiers dans le serveur distant - Faites votre choix            ");
   private JProgressBar progression = new JProgressBar();
   private final int BUFFER = 4096;

   public Client() {
      super("Archivage de fichiers");
      add(boutons, BorderLayout.NORTH);
      boutons.add(new AbstractAction("Archiver") {
         public void actionPerformed(ActionEvent e) {
            if (sélection.showSaveDialog(null) == JFileChooser.APPROVE_OPTION)
                new TransfertFichier(sélection.getSelectedFile()).execute();
         }
      });
      boutons.addSeparator();
      boutons.add(progression);
      boutons.addSeparator();
      progression.setStringPainted(true);
      progression.setVisible(false);
      résultat.setEditable(false);
      résultat.setMargin(new Insets(3, 3, 3, 3));
      add(résultat, BorderLayout.SOUTH);
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Client(); }
   
   private class TransfertFichier extends SwingWorker<String, Integer> {
      private File fichier;
      private int octetsLus;

      public TransfertFichier(File fichier) {
         this.fichier = fichier;
      }
      
      @Override
      protected String doInBackground() throws Exception {
         try {
            résultat.setText("Le fichier ("+fichier.getName()+") est en cours de transfert");
            progression.setVisible(true);
            progression.setValue(0);
            BufferedInputStream lecture = new BufferedInputStream(new FileInputStream(fichier));
            progression.setMaximum(lecture.available());
            Socket service = new Socket("localhost", 5588);
            ObjectOutputStream envoi = new ObjectOutputStream(new BufferedOutputStream(service.getOutputStream()));
            envoi.writeObject(fichier.getName());
            envoi.writeInt(lecture.available());
            envoi.writeInt(BUFFER);
            byte[] octets = new byte[BUFFER];
            while (lecture.available() > 0) {
               if (lecture.available() < BUFFER) octets = new byte[lecture.available()];
               lecture.read(octets);
               envoi.write(octets);
               envoi.flush();
               publish(octetsLus += BUFFER);
            }
            lecture.close();
            service.close();
            return "Le fichier ("+fichier.getName()+") est entièrement transféré";
         }
         catch (FileNotFoundException ex) {
            return "ATTENTION : Le fichier n'existe pas";
         }
         catch (IOException ex) {
            return "ATTENTION : Impossible d'envoyer le fichier";
         }
      }

      @Override
      protected void process(List<Integer> nombres) {
         progression.setValue(nombres.get(nombres.size()-1));
      }
      
      @Override
      protected void done() {
         try {
            résultat.setText(get());
            progression.setVisible(false);
         }
         catch (Exception ex) { }
      }     
   }
}
Codage correspondant côté serveur
package réseau;

import java.io.*;
import java.net.*;

public class Serveur  {
   private static String stockage = "G:/Stockage/";
   
   public static void main(String[] args) throws IOException, ClassNotFoundException {
      ServerSocket service = new ServerSocket(5588);
      while (true) {
         Socket client = service.accept();
         try {
            ObjectInputStream réception = new ObjectInputStream(new BufferedInputStream(client.getInputStream()));
            String nom = (String) réception.readObject();
            int taille = réception.readInt();
            int buffer = réception.readInt();
            int nombre = taille / buffer;
            byte[] octets = new byte[buffer];
            byte[] reste = new byte[taille % buffer];
            BufferedOutputStream fichier = new BufferedOutputStream(new FileOutputStream(stockage + nom));
            for (int i = 0; i < nombre; i++) {
               réception.readFully(octets);
               fichier.write(octets);
            }
            réception.readFully(reste);
            fichier.write(reste);
            fichier.close();
            client.close();
         } 
         catch (Exception ex) {}         
      }  
   }
}

 

Choix des chapitres Lancer un autre processus à partir d'un programme Java

Nous avons vu combien il était facile de créer et de manipuler des threads multiples s'exécutant au sein d'un même interpréteur Java. Java possède également une classe java.lang.Process qui représente un autre programme s'exécutant de manière externe à l'interpréteur. Un programme Java peut communiquer ensuite avec un processus externe en utilisant des flux de la même manière qu'il peut communiquer avec un serveur sur un autre ordinateur du réseau. L'utilisation d'un objet Process est toujours dépendant de la plate-forme et rarement portable, mais s'avère parfois utile.

Pour communiquer entre ordinateurs, voir la leçon sur les Sockets.
§

En fait, pour exécuter un autre programme (ou autre processus) depuis notre programme actuel, il est nécessaire d'utiliser deux classes. La première est Process comme nous venons de le voir et qui est utile pour lancer un nouveau processus à partir du processus actuel. La seconde est java.lang.Runtime qui permet de connaitre toutes les informations relatives à l'environnement de la machine comme par exemple le système d'exploitation.

java.lang.Process

Cette classe décrit un processus à exécuter dans un environnement externe à l'interpréteur Java. Notez qu'un objet Process est tout à fait différent d'un objet Thread ; la classe Process est abstraite et ne peut pas être instanciée. Appelez alors l'une des méthodes Runtime.exec() pour démarrer un processus et retourner l'objet Process correspondant.

La classe java.lang.Process
InputStream getInputStream()
Récupère le flot de données à lire depuis l'autre programme.
OutputStream getOutputStream()
Récupère le flot de données à envoyer vers l'autre programme.
int waitFor()
Le programme actuel se met en attente jusqu'à ce que le processus demandé s'arrête.

java.lang.Runtime

Cette classe encapsule un certain nombre de fonctions système dépendant de la plate-forme. C'est cette classe qui permet de connaitre notamment le système d'exploitation utilisé ainsi que ses commandes annexes.

La classe java.lang.Runtime
static Runtime getRuntime()
Retourne l'objet Runtime de la plate-forme actuelle ; cet objet peut exécuter des fonctions système d'une manière indépendante de la plate-forme.
Process exec(String commande)
Démarre un nouveau processus s'exécutant dans un environnement externe à l'interpréteur. Notez que tous les processus qui s'exécutent hors de l'environnement Java peuvent être dépendants du système.
long freeMemory()
Retourne la quantité approximative de la mémoire libre.
long totalMemory()
Retourne la quantité totale de mémoire disponible au sein de l'interpréteur Java.

Lancer un autre processus

Après toutes ces considérations, nous allons demander dans notre processus principal de récupérer toutes les informations concernant le réseau grâce à la commande ipconfig du système d'exploitation. Toutes ces informations seront ensuite récupérées dans un flot de texte d'entrée et ensuite envoyées dans le flot de sortie principal, c'est à dire sur l'écran.

Il est bien évident que ce programme ne sert absolument à rien puisque il suffit de tapez directement cette commande depuis le système d'exploitation pour obtenir exactement le même résultat. Toutefois, ce petit programme simple nous permet de comprendre tous les mécanismes en jeu.

Code du processus principal

Résultat obtenu

 

Choix des chapitres Chronomètres

Dans un certain nombre d'environnement de programmation, vous pouvez définir des chronomètres. Un chronomètre sert à prevenir les éléments de votre programme à intervalles réguliers. Par exemple, pour afficher une horloge dans une fenêtre, l'objet horloge doit être averti une fois par seconde.

  1. Swing possède une classe chronomètre intégrée (Timer) qui est facile à utiliser. Vous pouvez construire un chronomètre en fournissant un objet d'une classe qui implémente l'interface ActionListener (permet de préciser l'objet qui est en écoute d'un événement particulier comme ici le temps écoulé), et le délai entre deux alertes du chronomètre, en millisecondes.

  2. Ensuite la méthode actionPerformed de la classe de l'écouteur est appelée automatiquement sur le thread de répartition des événements et ainsi vous pouvez préciser ce que vous désirez faire à chaque top d'horloge.

La classe javax.swing.Timer
Timer(int délai, ActionListener écouteur)
Crée un nouveau chronomètre qui envoie des événement temporels à un écouteur. délai : le délai, en milliseconde, entre deux événement. écouteur : écouteur d'action qui doit être averti lorsque le délai est écoulé.
void start()
Lance le chronomètre. Après cet appel, le chronomètre commence à envoyer des événements à son écouteur d'action.
void stop()
Arrête le chronomètre. Après cet appel, le chronomètre arrête d'envoyer des événements à son écouteur d'action.
void setInitialDelay(int délai)
Spécifie le temps en millisecondes entre l'invocation de la méthode start() et le déclenchement du premier ActionEvent.
void setRepeats(boolean plusieurs)
Spécifie si le timer doit envoyer un événement périodiquement. Si plusieurs = false, un seul événement est envoyer après l'appel de la méthode start(). Par défaut, plusieurs = true.

A titre d'exemple, le programme suivant indique le temps écoulé en secondes depuis le lancement de l'application.

Bien qu'il y ait très peu de chose à écrire, il est possible de faire appel à l'expert "Implémenter interface..." pour construire automatiquement toute l'ossature, à la fois pour ActionListener, et également pour actionPerformed.


Vous allez mettre en oeuvre l'application ci-contre qui consiste à faire défiler un texte de la droite vers la gauche. Lorsque le texte disparaît complètement à gauche, il réapparaît tout de suite à droite. Il est possible de changer le contenu de votre texte défilant. Vous disposez alors d'une zone de saisie en bas, et il suffit de validez avec le bouton "Changer" pour que votre nouveau texte défile à son tour à la même position que l'ancien texte.

Pour cette applicaiton, vous n'utliserez pas la classe Timer, mais tout simplement un thread classique qui s'endort périodiquement.

Par ailleurs, vous avez besoin de connaître la largeur de votre texte, et ceci quelque soit la taille et le choix de votre fonte. Heureusement, il existe une classe FontMetrics qui encapsule toutes les informations relatives à la fonte utilisée comme la police, la taille, ... Cette classe dispose, entre autre, d'une méthode stringWidth qui fourni la largeur en pixel d'un texte donné par rapport à la fonte utilisée. Tous les composants graphiques comme, par exemple, JLabel disposent de beaucoup de propriétés, notamment la fonte du composant qu'il est possible de récupérer grâce à la méthode getFont (get pour récupérer une propriété), et également la propriété relative aux dimensions de la fonte que l'on récupère avec la méthode getFontMetrics.

Vous allez également mettre en oeuvre la représentation graphique simplifiée d'une horloge. Pour cela, vous aurez besoin de la classe Date qui permet de récupérer à la fois la date et l'heure données par le système. Cette classe Date dispose de méthodes (obsolètes, mais ce n'est pas grave) qui permettent de récupérer l'heure, la minute et la seconde en cours. N'oubliez pas que vous disposez d'une classe Math qui contient toutes les méthodes statiques nécessaires aux calculs mathématiques comme, par exemple, Math.cos, Math.sin, Math.PI, ...