1. Généralités

1-A. Qu'est-ce qu'un thread

Pour éviter toute ambiguïté, il est important de préciser qu'un thread n'est pas un processus. En effet, les processus vivent dans des espaces virtuels isolés alors que les threads sont des traitements qui vivent ensemble au sein d'un même processus.

Les threads partagent la même mémoire contrairement aux processus.

Un thread est donc une portion de code capable de s'exécuter en parallèle à d'autres traitements. Ils sont utiles dans bien des cas et parfois même nécessaires comme nous le verrons plus loin dans la section à propos de Swing.

Ils servent à maintes choses par exemple :

  • faire des traitements en tâche de fond, c'est le cas de la coloration syntaxique des éditeurs ;
  • exécuter plusieurs instances d'un même code pour accélérer le traitement, pour de longs traitements n'utilisant pas les mêmes ressources ;
  • s'adresser de manière personnelle à plusieurs clients simultanément comme les serveurs HTTP ou les chats ;
  • résoudre certaines problématiques liées au mode de fonctionnement de Swing.

Contrairement à une autre idée reçue, les threads ne s'exécutent pas en même temps, mais en temps partagé, c'est pour cette raison qu'il est important pour un thread de toujours laisser une chance aux autres de s'exécuter.

Ce n'est pas obligatoire sous Windows, car l'OS utilise un système de gestion de threads dit préemptif, c'est-à-dire qu'il donne et reprend lui-même une fenêtre d'exécution.

Le schéma ci-dessous illustre les temps d'exécution et de latence des threads.

Image non disponible

1-B. Cycle de vie d'un thread

Le thread peut avoir quatre états différents, mais deux seulement peuvent être testés.

1-B-1. État nouveau

C'est l'état initial après l'instanciation du thread. À ce stade, le thread est opérationnel, mais celui-ci n'est pas encore actif.

Un thread prend cet état après son instanciation.

1-B-2. État exécutable

Un thread est dans un état exécutable à partir du moment où il a été lancé par la méthode start() et le reste tant qu'il n'est pas sorti de la méthode run().

Dès que le système le pourra, il donnera du temps d'exécution à votre thread.

1-B-3. État en attente

Un thread en attente est un thread qui n'exécute aucun traitement et ne consomme aucune ressource CPU. Il existe plusieurs manières de mettre un thread en attente. Par exemple :

  • appeler la méthode thread.sleep (temps en millisecondes) ;
  • appeler la méthode wait() ;
  • accéder à une ressource bloquante (flux, accès en base de données, etc.) ;
  • accéder à une instance sur laquelle un verrou a été posé ;
  • appeler la méthode suspend() du thread.

La méthode suspend() ne doit plus être utilisée, elle est dépréciée. Nous verrons pourquoi un peu plus tard.

Un thread en attente reste considéré comme exécutable.

1-B-4. État mort

Un thread dans un état mort est un thread qui est sorti de sa méthode run() soit de manière naturelle, soit de manière subite (Exception non interceptée).

1-C. L'interface Runnable

Avant de parler de la classe thread, nous allons étudier l'interface Runnable du package java.lang.

L'interface Runnable nous met à disposition une unique méthode, la méthode run dont la signature est la suivante :

Signature de la méthode
Sélectionnez
 runpublic void run() ;

Notre traitement doit se trouver dans cette méthode. Il ne peut pas prendre de paramètre et ne peut pas retourner de valeur.

Si nous avons besoin de paramètres, nous les transmettrons au constructeur qui se chargera de les stocker en variable d'instance.

Un accesseur pourra être mis en place pour récupérer une valeur à la fin du traitement.

1-D. La classe thread

La classe thread du package java.lang est celle qui doit impérativement être dérivée pour qu'une classe puisse être considérée comme un thread et donc, exécutable en parallèle.

Cette classe concrète implémente l'interface Runnable. Le code que vous désirez voir exécuter lors de l'activation doit donc être placé dans la méthode run() vue précédemment.

Les principaux constructeurs de la classe thread sont :

Principaux constructeurs
Sélectionnez
public Thread();
public Thread(Runnable runnable);

Le lancement d'un thread doit se faire par la méthode start().

Lancement d'un thread
Sélectionnez
MonThread t = new MonThread() ;
t.start();

Voici un exemple simple d'implémentation de thread.

Exemple
Sélectionnez
public class Monthread extends thread {
  public void run() {
    // faire quelque chose
  }
}
Exemple avec passage de paramètre
Sélectionnez
public class Monthread extends thread {
  private String str;
 
  public Monthread(String str) {
    this.str = str;
  }
  public void run() {
    // faire quelque chose avec str
  }
}

Implémenter l'interface Runnable n'est pas la même chose que faire un thread. Appeler la méthode run() d'une classe qui implémente cette interface ne lance pas non plus un thread, même si cette classe est un thread !

1-E. Mon premier thread

Dans ce premier exemple concret, nous allons apprendre à écrire un thread simple, à l'initialiser et à le lancer.

Pour démontrer que le thread est bien actif en même temps que la classe principale, celle-ci effectuera des affichages sur la console.

Source de du thread
Sélectionnez
package tutorialthread.example01;
 
public class Unthread extends thread{
  public void run() {
    long start = System.currentTimeMillis();
    // boucle tant que la durée de vie du thread est < à 5 secondes
    while( System.currentTimeMillis() < ( start + (1000 * 5))) {
      // traitement
      System.out.println("Ligne affichée par le thread");
      try {
        // pause
        thread.sleep(500);
      }
      catch (InterruptedException ex) {}
    }
  }    
}
Source de la classe de test
Sélectionnez
package tutorialthread.example01;
 
public class Main {
  public Main() {
    // création d'une instance du thread
    Unthread thread = new Unthread();
    // Activation du thread
    thread.start();
    // tant que le thread est en vie...
    while( thread.isAlive() ) {
      // faire un traitement...
      System.out.println("Ligne affichée par le main");
      try {
        // et faire une pause
        thread.sleep(800);
      }
      catch (InterruptedException ex) {}
    }
  }
 
  public static void main(String[] args) {
    new Main();
  }
 
}

2. Synchronisation des threads

Il est assez fréquent de devoir faire travailler un ensemble de threads en même temps. Tant que chaque thread ne fait que manipuler des données qui lui sont propres, cela ne pose aucun souci. Les ennuis arriveront dès que les données à manipuler sont communes.

Les données risquent leur intégrité si deux threads se retrouvent à les manipuler en même temps.

Pour illustrer ce cas, nous allons prendre l'exemple des vases communicants.

Deux vases initialement remplis d'une même quantité. À chaque itération, nous enlèverons X de l'un pour l'ajouter à l'autre.

Nous effectuerons un contrôle afin de s'assurer que la somme des deux correspond bien au volume initial.

threads désynchronisés
Sélectionnez
/*
 * Created on 8 juil. 2004
 *
 */
package tutorialthread.examples.sync;
 
import java.util.Random;
 
/**
 * @author Valère VIANDIER
 *
 */
public class VaseComuniquant {
    private static final int QUANTITE_INITIALE = 200;
    private static final int NB_thread_MAX = 2;
 
    private static int iteration = 0;
 
    private int[] vase = {QUANTITE_INITIALE / 2,QUANTITE_INITIALE / 2};
 
    public VaseComuniquant() {
        for( int i = 0; i < NB_thread_MAX; i++)
            new threadTransfert().start();
    }
    public static void main(String[] args) {
        new VaseComuniquant();
    }
 
    public int transfert(int qte) {
        // Ne pas enlever les System.out de ce test !
        System.out.print("-("+qte+") dans le vase 1 ");
        vase[0] -= qte;
        System.out.println("+("+qte+") dans le vase 2");
        vase[1] += qte;
        iteration++;
        if( iteration % 1000 == 0)
            System.out.println("" + iteration + " itérations.");
        return vase[0]+vase[1];
    }
 
    public class threadTransfert extends thread {
        Random r = new Random();
        int quantite;
        public void run() {
            while( !isInterrupted()) {
                quantite = r.nextInt(11)-6;
                vase[0] -= quantite;
                vase[1] += quantite;
                if( transfert(quantite) != QUANTITE_INITIALE) {
                    System.err.println("Quantité totale invalide à l'itération " + iteration);
                    System.exit(-1);
                }
 
                try {
                    thread.sleep(10);
                } catch (InterruptedException e) {}
            }
        }
 
    }
}

Si vous exécutez ce code, vous risquez fort d'avoir une sortie violente provoquée avant les 500 itérations.

En jouant avec la valeur de NB_thread_MAX, les résultats peuvent varier sachant qu'avec une valeur de 1, il n'y a aucun risque d'erreur.

Plus vous aurez de threads et plus le traitement sera long, plus vous aurez de chances de casser l'intégrité de vos données.

Afin de pallier ce problème, il nous suffit de déclarer la méthode transfert synchronisée comme ceci :

 
Sélectionnez
public synchronized int transfert(int qte) {
  ...
}

Le mot-clé synchronized indique qu'un et un seul thread peu accéder en même temps à cette méthode. Ainsi, les données manipulées ne risquent pas d'être altérées.

Attention, l'usage abusif de méthodes synchronisées peut considérablement réduire les performances de vos applications.

Il existe une autre manière de synchroniser les données que nous verrons dans la seconde partie de ce tutoriel.

3. Swing et les threads

Swing utilise une pile d'événements appelée EventQueue.

Le mécanisme en est très simple et une fois que l'on a compris son fonctionnement, beaucoup de problématiques de Swing trouvent une explication donc, une solution.

Quelques rappels à propos de la spécification de la machine virtuelle :

  • les événements doivent être dépilés dans le même ordre ;
  • les événements ne doivent pas être dispatchés en même temps.

Nous allons illustrer cette partie de la spécification de la JVM au moyen de deux programmes simples, l'un écrit d'une manière dite " naturelle " et l'autre, en utilisant un thread !

Pour cela, reprenons le problème récurrent de l'affichage d'une barre de progression pendant un traitement long.

Notre traitement sera représenté ici par une classe faisant 100 itérations inutiles, mais composées d'une pause de 100 ms.

Voici le code de notre traitement

 
Sélectionnez
/*
 * Created on 7 juil. 2004
 *
 */
package tutorialthread.examples.swing;
 
import javax.swing.JProgressBar;
 
/**
 * @author Valère VIANDIER
 *
 */
public class LongAction {
 
    public void traitementLong(JProgressBar pBar) {
        // initialisation de la progressBar au début du traitement
        pBar.setMinimum(0);
        pBar.setMaximum(99);
        pBar.setValue(0);
 
        // Traitement
        for( int i = 0; i < 100; i++ ) {
            System.out.print(".");
            pBar.setValue(i);
            try {
                // Pause pour simuler un traitement
                thread.sleep(100);
            } catch (InterruptedException e) {}
        }
        System.out.println("") ;
    }
}
Code de la classe de test
Sélectionnez
/*
 * Created on 7 juil. 2004
 *
 */
package tutorialthread.examples.swing;
 
import java.awt.*;
import java.awt.event.*;
 
import javax.swing.*;
 
/**
 * @author Valère VIANDIER
 *
 */
public class TestTraitementLong extends JFrame {
  JProgressBar jBar = new JProgressBar();
  GridLayout layout = new GridLayout(2,1);
  JButton btnAction = new JButton("Lancer le traitement");
 
  public TestTraitementLong() {
    // Construction de la fenêtre de test
    Container contentPane = getContentPane();
    contentPane.setLayout(layout);
    contentPane.add(jBar);
    contentPane.add(btnAction);
    setTitle("Test de progressBar");
    // Ajout du listener du bouton
    btnAction.addActionListener(
      new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          // Instanciation et lancement du traitement
          LongAction action = new LongAction();
          action.traitementLong(jBar);
        }
      }
    );
 
    // Affichage de la fenêtre
    pack();
    setVisible(true);
 
  }
 
  public static void main(String[] args) {
    new TestTraitementLong();
  }
}

Si vous lancez cette application, vous pourrez constater un certain nombre de choses :

  • la barre de progression n'avance pas ;
  • la console affiche bien les points ce qui indique que le traitement s'effectue ;
  • si vous agrandissez la fenêtre pendant le traitement, celle-ci ne se rafraîchit pas ;
  • à la fin du traitement, la barre de progression affiche 100 % et la fenêtre se redessine correctement.

En introduction de ce chapitre, nous disions que les événements ne devaient être dépilés que « un par un ».

Dans notre exemple, le premier événement explicitement dépilé est celui du bouton. Tant que le code du bouton n'est pas terminé, aucun autre événement ne sera dépilé !

Du coup, à chaque modification de l'état de la barre de progression, l'événement lié au rafraîchissement du composant graphique est empilé et attend que le code du bouton soit achevé pour être dépilé à son tour.

Nous pouvons donc dire que notre traitement empile 100 événements de rafraîchissement.

Si en plus vous avez redimensionné la fenêtre, cela fait autant d'événements en attente dans la pile d'événements.

Pour résoudre ce problème, il suffit que l'événement du bouton ne soit plus bloquant. Pour cela, nous allons utiliser un thread afin de rendre le plus rapidement possible la main à l'EventQueue de Swing.

Je vous rassure tout de suite, il est inutile de changer tous vos traitements pour les placer dans des threads, nous allons créer un thread dynamiquement et appeler votre traitement dans celui-ci, ainsi, aucune modification n'est nécessaire dans le code de votre méthode.

Voici le code du Listener du bouton modifié.

Code du listener
Sélectionnez
btnAction.addActionListener(
  new ActionListener() {
    public void actionPerformed(ActionEvent e) {
      // création d'un thread d'exécution
      thread t = new thread() {
        public void run() {
          // Instanciation et lancement du traitement
          LongAction action = new LongAction();
          action.traitementLong(jBar);
        }
      };
      t.start();
    }
  }
);

4. Conclusion

La programmation des threads n'est pas fondamentalement complexe, mais nécessite une très bonne compréhension. Se lancer dans le développement multithreads sans comprendre les mécanismes de ceux-ci est un risque important.

Dans l'exemple sur la synchronisation, essayez de supprimer les affichages à la console que j'ai volontairement insérés entre l'addition et la soustraction. Il y a fort à parier que vous n'aurez pas de désynchronisation avant plusieurs milliers d'itérations, voire aucune désynchronisation du tout.

Mais pour peu que quelque chose perturbe le lancement des N threads et que ceux-ci soient calés différemment dans le temps…

Dans la prochaine partie, nous traiterons la notification entre les threads, une autre méthode de synchronisation, nous verrons aussi ce qu'est un thread d'exécution et à quoi il peut bien nous servir, la classe threadLocal ainsi que d'autres choses.