Accueil
Rechercher:
sur developpez.com sur les forums
Forums | Tutoriels | F.A.Q's | Participez | Hébergement | Contacts
Club Emploi Blogs   TV   Dév. Web PHP XML Python Autres 2D-3D-Jeux Sécurité Windows Linux PC Mac
Accueil Conception Java DotNET Visual Basic  C  C++ Delphi MS-Office SQL & SGBD Oracle  4D  Business Intelligence
FORUMS JAVA FAQs TUTORIELS JAVASEARCH SOURCES LIVRES OUTILS, EDI & API ECLIPSE NETBEANS BLOG DISCUSSIONS TV

Programmation des Threads en Java, première partie

19/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. Généralités



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.


1.2.1. Etat nouveau


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.


1.2.2. Etat 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.2.3. Etat 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.2.4. Etat 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.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.


1.4. 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é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() { // faire quelque chose } }
Exemple avec passage de paramètre
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.5. 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
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
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.
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) { // 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 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.


3. Swing et les Thread


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) { // 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
/* * 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î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) { // 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 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.



Responsables bénévoles de la rubrique Java : Eric Siber et Baptiste Wicht - Contacter par EMail :
Vos questions techniques : forum d'entraide Java - Publiez vos articles, tutoriels et cours
et rejoignez-nous dans l'équipe de rédaction du club d'entraide des développeurs francophones
Nous contacter - Copyright © 2000-2008 www.developpez.com - Legal informations.