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.
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 :
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 :
public
Thread
(
);
public
Thread
(
Runnable runnable);
Le lancement d'un thread doit se faire par la méthode start().
MonThread t =
new
MonThread
(
) ;
t.start
(
);
Voici un exemple simple d'implémentation de thread.
public
class
Monthread extends
thread {
public
void
run
(
) {
// faire quelque chose
}
}
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.
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) {}
}
}
}
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 nous assurer que la somme des deux correspond bien au volume initial.
/*
* 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 :
public
synchronized
int
transfert
(
int
qte) {
...
}
Le mot-clé synchronized indique qu'un et un seul thread peut 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
/*
* 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
(
""
) ;
}
}
/*
* 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é.
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.