|
Les threads |
Définition d'un
thread
La création d'un thread
Les états d'un thread
La synchronisation des threads
La classe Thread
L'environnement de la Machine Virtuelle Java est multi-threads. L'équivalent de thread pourrait être tâche en français, mais pour éviter la confusion avec la notion de système multitâches, on emploiera le mot thread plutôt que tâche. Le fait que Java permettent de faire tourner en parallèle plusieurs threads lui donne beaucoup d'intérêt : Ceci permet par exemple de lancer le chargement d'une image sur le Web (ce qui peut prendre du temps), sans bloquer votre programme qui peut ainsi effectuer d'autres opérations.
Pour vous montrer les possibilités du multi-threading voici une applet qui peut lancer trois horloges tournant en parallèle (au passage, voici une démonstration de réutilisation de la classe TimeCounter déjà utilisée au premier chapitre).
Ne confondez pas le multi-threads avec le traitement événementiel grâce à un timer, dont le résultat rendrait la même chose. Dans cette applet, il y a réellement trois threads différents qui gèrent chacun une horloge mise à jour à intervalle régulier. La mise à jour n'est pas déclenchée suite à un événement du type WM_TIMER émis régulièrement par le gestionnaire de fenêtres.
Plusieurs aspects des threads sont à étudier pour bien comprendre leur fonctionnement et leur utilisation : la gestion par la Machine Virtuelle Java pour répartir le temps d'exécution entre les différents threads, la manière de créer un thread, les différents états possibles d'un thread, et la synchronisation entre threads pour le partage de données.
L'environnement Java est multi-threads, et le langage permet d'utiliser cette fonctionnalité pour créer vos propres threads, et synchroniser des threads qui partagent des données.
Le partage du temps entre threads
Comment faire tourner plusieurs threads en même temps alors que votre ordinateur ne possède qu'un seul microprocesseur ? La réponse vient de manière assez évidente : A tout moment, il n'y a en fait qu'un seul thread en cours d'exécution, et éventuellement d'autres threads en attente d'exécution.
Si le système permet de partager le temps d'exécution entre les différents threads, il leur donne chacun leur tour un petit temps d'exécution du processeur (quelques millisecondes). Sinon, chaque fois qu'un thread a terminé d'exécuter une série d'instructions et qu'il cède le contrôle au système, le système exécute un des threads en attente, et ainsi de suite... Si la série d'instructions qu'exécute chaque thread prend un temps assez court, l'utilisateur aura l'illusion que tous les threads fonctionnent ensemble.
Le seul contrôle que peut avoir le programmeur sur la gestion de l'ordre dans lequel les threads en attente s'exécuteront, s'effectue en donnant une priorité à chaque thread qu'il crée. Quand le système doit déterminer quel thread en attente doit être exécuté, il choisit celui avec la priorité la plus grande, ou à priorité égale, celui en tête de file d'attente.
Sur certains systèmes, la Machine Virtuelle Java ne partage pas d'elle-même le temps du processeur entre les threads susceptibles d'être exécutés. Si vous voulez que vos programmes Java se comportent similairement sur tous les systèmes, vous devez céder le contrôle régulièrement dans vos threads avec les méthodes telles que sleep () ou yield (), pour permettre aux autres threads en attente, de s'exécuter. Sinon, tant que l'exécution d'un thread n'est pas interrompue, les autres threads resteront en attente !
Il existe deux moyens d'écrire des threads dans un programme : Les deux moyens passent par l'écriture d'une méthode run () décrivant les instructions que doit exécuter un thread. Cette méthode run () est soit déclarée dans une classe dérivée de la classe Thread , soit déclarée dans n'importe quelle classe qui doit alors implémenter l'interface Runnable (cette interface ne décrit que la méthode run ()). La seconde méthode est très utile : elle permet d'ajouter les fonctionnalités des threads à une classe existante, dans une classe dérivée.
Par exemple, elle est utilisée par la classe TimeCounter qui dérive de la classe Counter et implémente l'interface Runnable. La classe Counter a d'abord été créée pour des besoins simples : Avoir à disposition un compteur digital au look sympa, avec des méthodes telles que incrementCounter () ou decrementCounter (). Puis, le besoin s'est fait sentir d'avoir un compteur de temps : il a suffi donc de créer une classe TimeCounter dérivée de Counter, et d'y ajouter (entre autres) la méthode run (), qui incrémente toutes les secondes le compteur.
Une autre démarche aurait pu être de créer la classe TimeCounter en la dérivant de la classe Thread, et d'y ajouter en utilisant la composition un champ de classe Counter, et une méthode run () effectuant les mêmes opérations. Mais, en conception orientée objet, cette seconde démarche semblait moins naturelle : En effet, un objet TimeCounter est plutôt comme tout objet Counter, un objet graphique auquel il a été ajouté la possibilité de s'incrémenter automatiquement toutes les secondes.Un certain nombre de méthodes sont nécessaires pour contrôler l'exécution d'un thread. Elles sont toutes décrites au paragraphe décrivant la classe Thread, mais en voici les principales :
- La classe Thread dispose principalement de deux sortes de constructeurs : Thread () et Thread (Runnable objetRunnable).
Quand vous créez un objet instance d'une classe ClasseThread dérivée de Thread, le premier est appelé implicitement par le constructeur de ClasseThread.
Le second est utilisé quand vous voulez créer un objet de classe Thread dont la méthode run () à exécuter se trouve implémentée dans un classe implémentant l'interface Runnable. Au passage, vous noterez que le paramètre du second constructeur doit être une référence d'interface Runnable : En fait, vous passerez en argument une référence sur un objet d'une classe ClasseRunnable implémentant Runnable ; ceci est un exemple de conversion d'une référence d'une classe ClasseX dans une référence d'interface InterfaceY, si ClasseX implémente l'interface InterfaceY, appliquée avec ClasseRunnable pour ClasseX et Runnable pour InterfaceY.- start () : Cette méthode permet de démarrer effectivement le thread sur lequel elle est invoquée, ce qui va provoquer l'appel de la méthode run () du thread. Cet appel est obligatoire pour démarrer l'exécution d'un thread. En effet, la création d'un objet de classe Thread ou d'une classe dérivée de Thread (par exemple, grâce à l'appel new Thread ()) ne fait que créer un objet sans appeler la méthode run ().
- stop () : Cette méthode permet d'arrêter un thread en cours d'exécution. Elle est utile principalement pour stopper des threads dont la méthode run () ne se termine jamais (comme par exemple, dans la classe TimeCounter, la méthode run () n'a pas de raison de s'arrêter d'elle-même puisqu'aux dernières nouvelles, on n'a pas encore trouver le moyen d'arrêter le temps !).
- sleep () : Ces méthodes static permettent d'arrêter le thread courant pendant un certain laps de temps, pour permettre ainsi à d'autres threads en attente, de s'exécuter. Par exemple, une fois mis à jour une horloge par un thread, celui-ci peut arrêter son exécution pendant une minute, en attendant la prochaine mise à l'heure.
- yield () : Cette méthode static permet au thread courant de céder le contrôle pour permettre à d'autres threads en attente, de s'exécuter. Le thread courant devient ainsi lui-même en attente, et regagne la file d'attente. De manière générale, vos threads devraient s'arranger à effectuer des séries d'instructions pas trop longues ou à entrecouper leur exécution grâce à des appels aux méthodes sleep () ou yield ().
Il faut éviter de programmer des séries d'instructions interminables sans appel à sleep () ou yield (), en pensant qu'il n'y aura pas d'autres threads dans votre programme. La Machine Virtuelle Java peut avoir elle aussi des threads système en attente et votre programme s'enrichira peut-être un jour de threads supplémentaires...- setPriority () : Permet de donner une priorité à un thread. Le thread de plus grande priorité sera toujours exécuté avant tous ceux en attente.
Pour illustrer l'utilisation des threads, voici un exemple d'un chronomètre affichant les 1/10 de seconde (Utiliser le bouton Arrêter pour l'arrêter et la redémarrer) :
et le programme Java correspondant (à copier dans un fichier dénommé Chrono.java et invoqué à partir d'un fichier HTML) :
import java.awt.*; import java.applet.Applet; public class Chrono extends Applet implements Runnable { private Thread chronometre; private int dixiemeseconde = 0; // Méthode appelée par le système au démarrage de l'applet public void start () { // Au début de l'applet, création et démarrage du chronomètre chronometre = new Thread (this); chronometre.start (); } public void run () { try { while (chronometre.isAlive ()) { // Dessine le compteur (appel indirect à la méthode paint ()), // puis l'augmente de 1 repaint (); dixiemeseconde++; // Arrête le thread pendant 1/10 s (100 ms) Thread.sleep (100); } } catch (InterruptedException e) { } } // Méthode appelée par le système à l'arrêt de l'applet public void stop () { // A la fin de l'applet, arrêt du chronometre chronometre.stop (); } // Méthode appelée par le système pour mettre à jour le dessin de l'applet public void paint (Graphics gc) { // Dessine le temps écoulé sous forme de hh:mm:ss:d en noir et helvetica gc.setColor (Color.black); gc.setFont (new Font ("Helvetica", Font.BOLD, size ().height)); gc.drawString (dixiemeseconde / 36000 + ":" + (dixiemeseconde / 6000) % 6 + (dixiemeseconde / 600) % 10 + ":" + (dixiemeseconde / 100) % 6 + (dixiemeseconde / 10) % 10 + ":" + dixiemeseconde % 10, 2, size ().height - 2); } }Ce programme s'utilisant sous forme d'applet, la classe Chrono dérive de Applet, et implémente l'interface Runnable. Comme décrit au chapitre sur les applets et aux chapitres suivants, la méthode paint () de la classe Applet est appelée pour mettre à jour le dessin apparaissant dans la fenêtre d'une applet : Ici, elle est outrepassée pour dessiner le chronomètre.
Quand l'applet est créée, une instance de la classe Chrono est allouée et la méthode start () créant le thread chronometre, est appelée. Si vous observez bien le comportement de cette applet, vous vous rendrez facilement compte que le chronomètre à une tendance à retarder... Ceci est normal : En effet, à chaque tour de boucle while (), le thread est arrêté pendant un dixième de seconde grâce à l'appel Thread.sleep (100), après le redessin de l'applet avec la méthode repaint () dans run (). Mais, le fait de redessiner le chronomètre prend un faible délai qui s'additionne au 1/10 de seconde d'arrêt du thread chronometre. Une programmation plus précise devrait notamment tenir compte de ce délai pour le soustraire de la valeur de 100 millisecondes passée à la méthode sleep (). La classe System déclare une méthode currentTimeMillis (), donnant le temps courant, qui peut vous y aider. A vous de jouer !...
Pendant son existence, un thread passe par plusieurs états qu'il est utile de connaître pour bien comprendre les threads et leurs possibilités. La figure suivante représente l'ensemble des états possibles d'un thread, et les transitions existantes pour passer d'un état dans l'autre. Ne vous inquiétez pas par sa complexité, vous aurez besoin essentiellement des méthodes décrites dans le paragraphe précédent (start (), stop (), yield () et sleep ()).
figure 10. Etats d'un threadQuand un nouveau thread est créé, il est dans l'état nouveau thread et ne devient exécutable qu'après avoir appelé la méthode start () sur ce nouveau thread.
Parmi tous les threads dans l'état exécutable, le système donne le contrôle au thread de plus grande priorité, ou à priorité égale, celui en tête de file d'attente, parmi les threads dans l'état exécutable. Le thread qui a le contrôle à un moment donné est le thread courant. Le thread courant en cours d'exécution cède le contrôle à un autre thread exécutable dans l'une des circonstances suivantes :
- A la fin de la méthode run (), le thread passe dans l'état mort.
- A l'appel de yield (), le thread passe dans l'état exécutable et rejoint la fin de la file d'attente.
- Sur les systèmes permettant de partager le temps d'exécution entre différents threads, le thread passe dans l'état exécutable après qu'un certain laps de temps se soit écoulé.
- En attentant que des opérations d'entrée/sortie (IO) se terminent, le thread passe dans l'état bloqué.
- A l'appel de sleep (), le thread passe dans l'état bloqué pendant le temps spécifié en argument, puis repasse à l'état exécutable, une fois ce délai écoulé.
- A l'appel d'une méthode synchronized sur un objet Objet1, si Objet1 est déjà verrouillé par un autre thread, alors le thread passe dans l'état bloqué tant que Objet1 n'est pas déverrouillé (voir la synchronisation).
- A l'invocation de wait () sur un objet, le thread passe dans l'état en attente pendant le délai spécifié en argument ou tant qu'un appel à notify () ou notifyAll () n'est pas survenu (voir la synchronisation). Ces méthodes sont déclarées dans la classe Object.
- A l'appel de stop (), le thread passe dans l'état mort.
- A l'appel de suspend (), le thread passe dans l'état bloqué, et ne redevient exécutable qu'une fois que la méthode resume () a été appelée.
Les deux derniers méthodes ne sont pas static et peuvent être invoquées aussi sur les threads exécutables qui ne sont en cours d'exécution.
Tous les threads d'une même Machine Virtuelle partage le même espace mémoire, et peuvent donc avoir accès à n'importe quels méthode ou champ d'objets existants. Ceci est très pratique, mais dans certains cas, vous pouvez avoir besoin d'éviter que deux threads n'aient accès n'importe quand à certaines données. Si par exemple, un thread threadCalcul a pour charge de modifier un champ var1 qu'un autre thread threadAffichage a besoin de lire pour l'afficher, il semble logique que tant que threadCalcul n'a pas terminé la mise à jour ou le calcul de var1, threadAffichage soit interdit d'y avoir accès. Vous aurez donc besoin de synchroniser vos threads.
Utilisation de synchronized
La synchronisation des threads se fait grâce au mot clé synchronized, employé principalement comme modificateur d'une méthode. Soient une ou plusieurs méthodes methodeI () déclarées synchronized, dans une classe Classe1 et un objet objet1 de classe Classe1 : Comme tout objet Java comporte un verrou (lock en anglais) permettant d'empêcher que deux threads différents n'aient un accès simultané à un même objet, quand l'une des méthodes methodeI () synchronized est invoquée sur objet1, deux cas se présentent :
- Soit objet1 n'est pas verrouillé : le système pose un verrou sur cet objet puis la méthode methodeI () est exécutée normalement. Quand methodeI () est terminée, le système retire le verrou sur Objet1.
La méthode methodeI () peut être récursive ou appeler d'autres méthodes synchronized de Classe1 ; à chaque appel d'une méthode synchronized de Classe1, le système rajoute un verrou sur Objet1, retiré en quittant la méthode. Quand un thread a obtenu accès à un objet verrouillé, le système l'autorise à avoir accès à cet objet tant que l'objet a encore des verrous (réentrance des méthodes synchronized).- Soit objet1 est déjà verrouillé : Si le thread courant n'est pas celui qui a verrouillé objet1, le système met le thread courant dans l'état bloqué, tant que objet1 est verrouillé. Une fois que objet1 est déverrouillé, le système remet ce thread dans l'état exécutable, pour qu'il puisse essayer de verrouiller objet1 et exécuter methodeI ().
Si une méthode synchronized d'une classe Classe1 est aussi static, alors à l'appel de cette méthode, le même mécanisme s'exécute mais cette fois-ci en utilisant le verrou associé à la classe Classe1.
Si Classe1 a d'autres méthodes qui ne sont pas synchronized, celles-ci peuvent toujours être appelées n'importe quand, que objet1 soit verrouillé ou non.Voici un exemple illustrant l'utilisation de méthodes synchronized, avec une applet affichant les résultats d'un calcul de courbes, quand ceux-ci sont valables. Comme cette applet fait des calculs continuels dans des boucles sans fin, elle n'est pas incluse dans cette page pour éviter de bloquer votre navigateur, mais essayez-là sur votre machine pour découvrir tout l'intérêt de la synchronisation des threads.
Voici le programme Java (à copier dans un fichier dénommé AfficheurDeCalcul.java et invoqué à partir d'un fichier HTML) :
import java.applet.Applet; import java.awt.*; public class AfficheurDeCalcul extends Applet { private Thread calculateur; private Thread afficheur; // Méthode appelée par le système au démarrage de l'applet public void start () { setBackground (Color.white); // Démarrage de deux threads l'un pour calculer une courbe, // l'autre pour l'affichage calculateur = new Calculateur (this); afficheur = new Afficheur (this); calculateur.start (); afficheur.start (); } // Méthode appelée par le système à l'arrêt de l'applet public void stop () { // Arrêt des deux threads afficheur.stop (); calculateur.stop (); } private int [ ] courbe; // calculerCourbe () et paint () sont // synchronized ce qui les empêche de fonctionner simultanément synchronized public void calculerCourbe () { // Création d'un tableau de la largeur de l'applet courbe = new int [size ().width]; // Calcul des points d'une sinusoïde avec fréquence aléatoire double pasCalcul = 2 * Math.PI / courbe.length / Math.random (); for (int i = 0; i < courbe.length; i++) courbe [i] = (int)( (Math.sin (i * pasCalcul) + 1) * size ().height / 2); } synchronized public void dessinerCourbe () { update (getGraphics ()); // update () efface le fond puis appelle paint () } // Méthode appelée par le système pour mettre à jour le dessin de l'applet synchronized public void paint (Graphics gc) { if (courbe != null) { // Dessin de la courbe en noir en reliant les points un à un gc.setColor (Color.black); for (int i = 1; i < courbe.length; i++) gc.drawLine (i - 1, courbe [i - 1], i, courbe [i]); } } } class Calculateur extends Thread { private AfficheurDeCalcul applet; public Calculateur (AfficheurDeCalcul applet) { this.applet = applet; } public void run () { while (isAlive ()) applet.calculerCourbe (); // Lance les calculs indéfiniment } } class Afficheur extends Thread { private AfficheurDeCalcul applet; public Afficheur (AfficheurDeCalcul applet) { this.applet = applet; } public void run () { while (isAlive ()) applet.dessinerCourbe (); // Lance les affichages indéfiniment } }Dans cette exemple, les méthodes calculerCourbe () et dessinerCourbe () de la classe AfficheurDeCalcul sont synchronized. Quand calculerCourbe () préparent les résultats dans le tableau courbe de l'applet, il ne faut pas qu'un autre thread puisse dessiner la courbe de cette applet en appelant dessinerCourbe (), et inversement !
L'objet représentant l'applet est verrouillé à l'appel de ces méthodes, pour bloquer tout autre thread tentant d'invoquer calculerCourbe () ou dessinerCourbe () sur cet objet. Les deux threads calculateur et afficheur s'interdisent ainsi mutuellement d'exécuter simultanément calculerCourbe () ou dessinerCourbe () sur l'objet calculateur. Si, en recopiant l'exemple, vous essayez de supprimer l'un ou les deux synchronized, vous verrez tout à coup que les courbes affichées ne sont plus très régulières, preuve que la courbe est modifiée pendant son affichage !...
A l'usage, vous vous rendrez compte que les boucles sans fin (ou presque !) des méthodes run () fonctionnent généralement correctement mais ont une forte tendance à plomber les performances du système, car elles tournent continuellement sans s'arrêter... Le programme se présente ainsi par soucis de simplification et pour que vous puissiez vous rendre compte que deux threads qui s'ignorent (ils ne s'appellent jamais l'un l'autre) peuvent se synchroniser pour accéder à un même objet pour obtenir un résultat correct.Le système ne crée pas de file d'attente pour les threads bloqués : Quand un objet est déverrouillé, n'importe quel thread bloqué sur cet objet est susceptible d'être débloqué pour avoir accès à une de ses méthodes synchronized. Dans l'exemple précédent, rien n'empêche en effet les méthodes calculerCourbe () ou dessinerCourbe () de s'exécuter plusieurs fois de suite, avant que l'autre méthode ne verrouille l'objet représentant l'applet et puisse s'exécuter. Pour vous le prouver, il vous suffit d'ajouter des appels à System.out.println (...) dans ces deux méthodes...
Il existe une autre syntaxe d'utilisation de synchronized :
class Classe1 { // ... void methode1 () { // ... synchronized (objet1) { // objet1 est verrouillé // jusqu'à la fin du bloc } } }Un thread peut accéder à un bloc synchronized, si objet1 n'est pas verrouillé par un autre thread. Si c'est le cas, comme pour une méthode synchronized, le thread est bloqué tant que objet1 n'est pas déverrouillé.
La Machine Virtuelle Java n'a pas de moyen de repérer les deadlocks (impasses) : Ceux-ci peuvent survenir quand par exemple, deux threads thread1 et thread2 restent dans l'état bloqué parce que thread1 attend qu'un objet objetX soit déverrouillé par thread2, alors que thread2 attend qu'un objetY soit déverrouillé par thread1. C'est à vous de faire attention dans votre programmation pour qu'un deadlock ne survienne pas.
Synchronisation avec wait () et notify ()
Comme il est expliqué dans l'exemple précédent, synchronized permet d'éviter que plusieurs threads aient accès en même temps à même objet, mais ne garantit pas l'ordre dans lequel ces méthodes seront exécutées par des threads. Pour cela, il existe plusieurs méthodes de la classe Object qui permettent de mettre en attente volontairement un thread sur un objet (avec les méthodes wait ()), et de prévenir des threads en attente sur un objet que celui-ci est à jour (avec les méthodes notify () ou notifyAll ()).
Ces méthodes ne peuvent être invoquées que sur un objet verrouillé par le thread courant, c'est-à-dire que le thread courant est en train d'exécuter une méthode ou un bloc synchronized, qui a verrouillé cet objet. Si ce n'est pas le cas, une exception IllegalMonitorStateException est déclenchée.Quand wait () est invoquée sur un objet objet1 (objet1 peut être this), le thread courant perd le contrôle, est mis en attente et l'ensemble des verrous d'objet1 est retiré. Comme chaque objet Java mémorise l'ensemble des threads mis en attente sur lui, le thread courant est ajouté à la liste des threads en attente de objet1. objet1 étant déverrouillé, un des threads bloqués parmi ceux qui désiraient verrouiller objet1, peut passer dans l'état exécutable et exécuter une méthode ou un bloc synchronized sur objet1.
Un thread thread1 mis en attente est retiré de la liste d'attente de objet1, quand une des trois raisons suivantes survient :
- thread1 a été mis en attente en donnant en argument à wait () un délai qui a fini de s'écouler.
- Le thread courant a invoqué notify () sur objet1, et thread1 a été choisi parmi tous les threads en attente.
- Le thread courant a invoqué notifyAll () sur objet1.
thread1 est mis alors dans l'état exécutable, et essaye de verrouiller objet1, pour continuer son exécution. Quand il devient le thread courant, l'ensemble des verrous qui avait été enlevé d'objet1 à l'appel de wait (), est remis sur objet1, pour que thread1 et objet1 se retrouvent dans le même état qu'avant l'invocation de wait ().
Un thread mis en attente en utilisant la méthode wait () sans argument sur un objet, ne peut redevenir exécutable qu'une fois qu'un autre thread a invoqué notify () ou notifyAll () sur ce même objet. Donc, wait () doit toujours être utilisé avec notify (), et être invoqué avant cette dernière méthode.
Par exemple, si dans l'exemple précédent, vous ajoutez à la fin de la méthode calculerCourbe (), vous ajoutez notifyAll () et au début de dessinerCourbe (), vous ajoutez wait (), vous obtiendrez ceci :public class AfficheurDeCalcul extends Applet { // ... synchronized public void calculerCourbe () { courbe = new int [size ().width]; double pasCalcul = 2 * Math.PI / courbe.length / Math.random (); for (int i = 0; i < courbe.length; i++) courbe [i] = (int)( (Math.sin (i * pasCalcul) + 1) * size ().height / 2); // Prévenir les autres threads du calcul d'une nouvelle courbe notifyAll (); } synchronized public void dessinerCourbe () { try { // Attendre qu'une nouvelle courbe soit calculée wait (); update (getGraphics ()); } catch (InterruptedException e) { } } // ... }A l'appel de wait () (ici sur l'objet this), le thread afficheur qui appelle la méthode dessinerCourbe () est mis en attente jusqu'à ce que ce que calculerCourbe () appelle notifyAll () pour prévenir les threads en attente qu'une nouvelle courbe est maintenant disponible. Ceci évite que dessinerCourbe () soit éventuellement exécutée plusieurs fois de suite alors qu'aucune nouvelle courbe n'a été calculée.
Il vaut mieux utiliser notifyAll () que notify (), car il est possible d'enrichir ce programme en créant par exemple des threads qui devront appeler dessinerCourbe () pour mettre à jour des fenêtres supplémentaires, ou en créant un autre thread appelant une méthode synchronized qui enregistre la courbe dans un fichier. Si la méthode notify () était appelée, un seul thread serait prévenu et mis à jour en ignorant les autres.Cette modification du programme n'empêche toujours pas la méthode calculerCourbe () de s'exécuter plusieurs fois de suite, car le thread calculateur qui appelle cette méthode, prévient en invoquant notifyAll () les threads en attente qu'une nouvelle courbe a été calculée, mais n'est jamais mis en attente pour laisser les autres threads utiliser ces nouveaux résultats. Une dernière modification des méthodes calculerCourbe () et dessinerCourbe () permet au thread afficheur appelant dessinerCourbe () de prévenir le thread calculateur en attente qu'il a terminé de dessiner la nouvelle courbe :
public class AfficheurDeCalcul extends Applet { // ... synchronized public void calculerCourbe () { try { // Attendre que les autres threads aient utilisé une courbe if (courbe != null) wait (); } catch (InterruptedException e) { } courbe = new int [size ().width]; double pasCalcul = 2 * Math.PI / courbe.length / Math.random (); for (int i = 0; i < courbe.length; i++) courbe [i] = (int)( (Math.sin (i * pasCalcul) + 1) * size ().height / 2); notifyAll (); } synchronized public void dessinerCourbe () { try { // Attendre qu'une courbe soit créée if (courbe == null) wait (); update (getGraphics ()); // Prévenir que la courbe a été utilisée courbe = null; notify (); } catch (InterruptedException e) { } } // ... }Ici, en modifiant le champ courbe avec null ou un nouveau tableau, un thread peut savoir si l'autre à terminer son traitement ou non. Si ce traitement n'est pas terminé, le thread se met en attente. Il redeviendra exécutable quand l'autre thread le préviendra qu'il a fini en appelant notify ().
Vous noterez que dessinerCourbe () mettant à null le champ courbe avant d'appeler notify (), il faudrait utiliser une autre logique de programmation pour que cette courbe reste disponible pour d'autres threads qui seraient créés.Cet exemple, utilisant deux boucles (sans fin) pour le calcul et l'affichage des courbes, entraîne une programmation d'un abord assez complexe. Mais, ceci n'est utilisé que pour des moyens de démonstration : Généralement, dans ce type de programme, un calcul est effectué ponctuellement sur commande et pas en boucle, et l'affichage attend la fin du calcul pour démarrer. Donc, la programmation avec uniquement synchronized comme dans la première version de cet exemple, suffira dans la plupart des cas pour synchroniser l'accès aux données.
La programmation de la synchronisation des threads est une tâche ardue, sur laquelle vous passerez sûrement du temps... Si vous désirez l'utiliser, il faut bien s'imaginer par quels états vont passer les threads, quelle implication aura l'utilisation des méthodes wait () et notify () sur l'ordre d'exécution des instructions du programme, tout en gardant bien à l'esprit que ces méthodes ne peuvent être invoquées que sur des objets verrouillés.
Le piège le plus classique est de se retrouver avec un deadlock parce que les threads sont tous en attente après avoir appelé chacun d'eux la méthode wait ().
Champs
public final static int MIN_PRIORITY public final static int NORM_PRIORITY public final static int MAX_PRIORITYCes constantes sont utilisées en argument de la méthode setPriority (), pour donner une priorité à vos threads (MIN_PRIORITY est égal à 1, NORM_PRIORITY à 5 et MAX_PRIORITY à 10). NORM_PRIORITY est la priorité par défaut d'un thread.
Constructeurs
public Thread () public Thread (Runnable target)Construit un nouveau thread à partir de la classe target implémentant l'interface Runnable. target doit implémenter la méthode run () qui sera la méthode exécutée au démarrage du nouveau thread créé.
public Thread (ThreadGroup group, Runnable target) throws SecurityException, IllegalThreadStateExceptionConstruit un nouveau thread à partir de la classe target avec comme groupe de thread group. La classe ThreadGroup permet de regrouper un ensemble de threads, et d'exécuter (stop (), suspend (),...) des méthodes sur tous les threads d'un groupe.
public Thread (String name) public Thread (ThreadGroup group, String name) throws SecurityException, IllegalThreadStateException public Thread (Runnable target, String name) public Thread (ThreadGroup group, Runnable target, String name) throws SecurityException, IllegalThreadStateExceptionMêmes constructeurs que précédemment avec un nom name.
Méthodes
La classe Thread compte un grand nombre de méthodes, dont voici la description des plus intéressantes. Pour pouvoir manipuler le thread courant que vous ne connaissez pas forcément, certaines de ces méthodes sont des méthodes de classe :
public static Thread currentThread ()Renvoie une référence sur le thread actuellement en cours d'exécution.
public synchronized void start () throws IllegalThreadStateExceptionProvoque le démarrage du thread sur lequel start () est invoqué puis l'appel à la méthode run (). Cette méthode rend la main immédiatement (le nouveau thread est lancé en parallèle au thread courant).
public void run ()run () est la méthode où sont décrites les instructions que doit exécuter un thread. Elle est appelée une fois que le thread a démarré. run () doit être outrepassée dans une classe dérivée de Thread ou une classe implémentant l'interface Runnable.
public final synchronized void stop () throws SecurityExceptionArrête le thread sur lequel stop () est invoqué.
public final void stop (Throwable exception) throws SecurityExceptionArrête le thread sur lequel stop () est invoqué en déclenchant l'exception exception.
public final boolean isAlive ()Renvoie true si un thread est vivant, c'est-à-dire que ce thread a démarré avec start () et n'a pas été arrêté soit avec stop (), soit parce qu'il a terminé d'exécuter toutes les instructions de la méthode run ().
public static void yield ()Permet de suspendre le thread courant pour laisser la main à d'autres threads en attente d'exécution.
public static void sleep (long millis) throws InterruptedException public static void sleep (long millis, int nanos) throws InterruptedExceptionProvoque l'arrêt du thread courant pendant millis millisecondes, ou pendant millis millisecondes et nanos nanosecondes. Ces méthodes sont susceptibles de déclencher une exception InterruptedException, qui n'est pas utilisée dans Java 1.0, mais vous oblige quand même à inclure l'appel à sleep () dans un try ... catch.
public final void suspend () throws SecurityExceptionSuspend l'exécution d'un thread vivant. Si plusieurs suspend () ont été invoquées sur un même thread, un seul resume () est nécessaire pour que ce thread reprenne son activité.
public final void resume () throws SecurityExceptionReprend l'exécution d'un thread, après une suspension avec suspend (). Si ce thread n'a pas été suspendu, le thread poursuit son exécution.
public void checkAccess ()Vérifie si le thread courant peut modifier le thread sur lequel checkAccess () est invoqué. Si cela lui est interdit, une exception SecurityException est déclenchée.
public final void setPriority (int newPriority) throws SecurityException, IllegalArgumentException public final int getPriority ()Modifie ou renvoie la priorité d'un thread. newPriority doit avoir une valeur comprise entre MIN_PRIORITY et MAX_PRIORITY.
public final void setName (String name) throws SecurityException public final String getName ()Modifie ou renvoie le nom d'un thread.
public final void setDaemon (boolean on) throws SecurityException, IllegalThreadStateException public final boolean isDaemon ()Permet de spécifier ou de savoir si un thread est un thread qui tourne en tâche de fond, pour rendre en général des services aux autres threads (daemon thread en anglais). Quand il n'a plus que des threads qui tournent en tâche de fond dans le système, la Machine Virtuelle Java s'arrête.
public final synchronized void join (long millis) throws InterruptedException public final synchronized void join (long millis, int nanos) throws InterruptedException public final void join () throws InterruptedExceptionCes méthodes provoquent l'arrêt du thread courant jusqu'à la mort du thread sur lequel est invoqué join (), et pendant un délai maximum de de millis millisecondes, ou millis millisecondes et nanos nanosecondes.
public static void dumpStack ()Imprime sur System.err l'état de la pile d'exécution du thread courant.
public final ThreadGroup getThreadGroup ()Renvoie le groupe de threads auquel appartient un thread.
public static int activeCount () public static int enumerate (Thread tarray [ ])Ces méthodes renvoient le nombre de threads actifs dans le groupe auquel appartient le thread courant, et la liste des threads actifs de ce groupe dans le tableau tarray ( tarray doit exister et être de taille supérieure ou égale à la valeur renvoyée par activeCount ()).
public void interrupt ()Cette méthode et les deux suivantes ne sont implémentées qu'à partir de Java 1.1. Cette méthode permet d'interrompre un thread. Si ce thread est en attente, il est exécuté et une exception InterruptedException est déclenchée.
public static boolean interrupted () public boolean isInterrupted () public String toString ()Renvoie une chaîne de caractère représentant un thread (comprenant son nom, sa priorité et le nom du groupe de thread auquel il appartient). Cette méthode outrepasse celle de la classe Object.
public void destroy () public int countStackFrames () throws IllegalThreadStateExceptionExemples
Application PaperBoardServer.
Applets Chrono, AfficheurDeCalcul, ObservateurCalcul, PaperBoardClient, AnimationFleche, ScrollText et Horloge.
|