Programmation des Threads en Java, première partie19/07/2004
Par
Valère VIANDIER Ce tutorial explique l'utilisation des Threads en Java
1. Généralités
1.1. Qu'est ce qu'un Thread
1.2. Cycle de vie d'un Thread
1.2.1. Etat nouveau
1.2.2. Etat exécutable
1.2.3. Etat en attente
1.2.4. Etat mort
1.3. L'interface Runnable
1.4. La classe Thread
1.5. Mon premier Thread
2. Synchronisation des Threads
3. Swing et les Thread
4. Conclusion
1.1. 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. Il 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 comme 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 long traitements n'utilisant pas les mêmes ressources.
- S'adresser de manière personnelle à plusieurs clients simultanément comme les serveurs HTTP ou les Chat.
- 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.

1.2. Cycle de vie d'un Thread
Le Thread peut avoir 4 états différents mais 2 seulement peuvent être testés.
C'est l'état initial après l'instanciation du Thread. A ce stade, le Thread est opérationnel mais celui-ci n'est pas encore actif.
Un thread prend cet état après son instanciation.
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.
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.
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.3. 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 run public 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.
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écuté lors de l'activation doit donc être
placé dans la méthode run() vue précédement.
Les principaux constructeurs de la classe Thread sont :
Principaux constructeurs public Thread();
public Thread(Runnable runnable);
Le lancement d'un Thread doit se faire par la méthode start().
Lancement d'un Thread MonThread t = new MonThread() ;
t.start();
Voici un exemple simple d'implémentation de Thread.
Exemple public class MonThread extends Thread {
public void run() {
}
}
Exemple avec passage de paramètre public class MonThread extends Thread {
private String str;
public MonThread(String str) {
this.str = str;
}
public void run() {
}
}
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 !
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 package tutorialthread.example01;
public class UnThread extends Thread{
public void run() {
long start = System.currentTimeMillis();
while( System.currentTimeMillis() < ( start + (1000 * 5))) {
System.out.println("Ligne affichée par le thread");
try {
Thread.sleep(500);
}
catch (InterruptedException ex) {}
}
}
}
Source de la classe de test package tutorialthread.example01;
public class Main {
public Main() {
UnThread thread = new UnThread();
thread.start();
while( thread.isAlive() ) {
System.out.println("Ligne affichée par le main");
try {
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.
2 vases initialement remplis d'une même quantité. A 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 /*
* 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) {
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 chance de casser l'intégrité de vos données.
Afin de pallier à ce problème, il nous suffit simplement de déclarer la méthode transfert synchronisée comme ceci :
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 tutorial.
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 êtres 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é d'une pause de 100 ms.
Voici le code de notre traitement /*
* 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) {
pBar.setMinimum(0);
pBar.setMaximum(99);
pBar.setValue(0);
for( int i = 0; i < 100; i++ ) {
System.out.print(".");
pBar.setValue(i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {}
}
System.out.println("") ;
}
}
Code de la classe de test /*
* 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() {
Container contentPane = getContentPane();
contentPane.setLayout(layout);
contentPane.add(jBar);
contentPane.add(btnAction);
setTitle("Test de progressBar");
btnAction.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
LongAction action = new LongAction();
action.traitementLong(jBar);
}
}
);
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îchie pas.
- A 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énement en attente dans la pile d'événements.
Pour résoudre ce problème, il suffit simplement 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 tout 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 btnAction.addActionListener(
new ActionListener() {
public void actionPerformed(ActionEvent e) {
Thread t = new Thread() {
public void run() {
LongAction action = new LongAction();
action.traitementLong(jBar);
}
};
t.start();
}
}
);
La programmation des threads n'est pas fondamentalement complexe mais nécessite une très bonne compréhension. Se lancer dans le
développement multi-threads 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é entre
l'addition et la soustraction. Il y a fort à parier que vous n'aurez pas de désynchronisation avant plusieurs milliers d'itération
voire, aucune désynchronisation du tout.
Mais pour peu que quelque chose perturbe le lancement des N threads et que ceux-ci soient callé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.
|