Dans les langages habituels, la programmation des plusieurs fils d'exécution qu'on appelle threads est généralement un domaine réservé du spécialiste du système utilisé. Bien peu de programmeurs s'y risquent ! Avec Java , la programmation des threads et si simple que l'on peut l'évoquer très tôt dans la présentation du langage.
Ceux qui ont déjà vus des applets ont pu se rendre compte qu'ils utilisent généralement de multiples threads. Par exemple, pendant que l'applet réalise une tâche précise, on y voit également des animations graphiques, de la musique etc.; et toutes ces tâches se déroulent en ``parallèle''.
Pour qui connaissent les processus (UNIX par exemple), on peut dire qu'un thread est un processus léger qui partagent le même espace de travail. Excepté les variables locales et les paramètres des méthodes qui sont dupliqués dans un espace propre à chaque thread, tout le reste est mis en commun pour tous les threads d'un même programme (mémoire, code et ressources partagés).
Comme les processus, les threads se trouvent, à tout instant, dans un état particulier. Ces divers états sont :
La première chose que l'on doit définir un filet d'exécution en parallèle, c'est évidemment les instructions que l'on veut faire exécuter dans ce filet. Java ne disposant pas de concept de pointeurs vers les fonctions, on effectue un petit détour pour arriver au résultat souhaité: on utilise une interface java.lang.Runnable qui définit une méthode unique run qui contient ces fameuses instructions que l'on veut faire exécuter au thread.
public abstract interface Runnable { public abstract void run(); }
La création du filet d'exécution en parallèle consiste donc à exécuter la méthode run sur un objet particulier; tout ce qui doit être exécuté doit être contenu cette méthode. La méthode publique run n'a pas d'argument et ne retourne rien. Il ne peut non plus lancer des exceptions. N'importe quelle classe peut donc contenir la méthode run et déclarant qu'elle implante l'interface Runnable.
class TestThread inplements Runnable { ... public void run() { ... } }
Une fois définies les instructions que l'on veut exécuter, il faut arriver à créer ce fameux filet d'exécution. Pour ce faire, on doit créer, comme on l'a déjà dit, un objet de la classe java.lang.thread. Il existe plusieurs constructeurs pour cette classe: en particulier, il en existe une qui prend en argument un objet qui implante la classe Runnable. La création d'un thread n'implique pas le démarrage de l'exécution des instructions qui lui sont associées : il existe mais n'est pas encore actif.
TestThread test = new TestThread (); Thread monThread = new Thread(test);
Une fois le thread crée, il faut le rendre actif explicitement pour pouvoir démarrer l'exécution de ce thread; ce qui se fait en invoquant la méthode start de la classe Thread.
monThread.start();
La méthode start est une méthode de la classe Thread qui alloue les diverses ressources nécessaires à l'exécution d'un thread et invoque la méthode run de l'objet passé en argument du constructeur de notre thread. Notre thread passe alors dans un état actif. Rendre actif un thread ne signifie pas qu'il s'exécute en continu jusqu'à la fin de son exécution : il ne fait que rejoindre les autres existants pour partager avec eux le temps d'exécution. Le système se charge de lui allouera régulièrement une tranche de temps pour qu'il puisse exécuter ces instructions. Ces tranches de temps alloués à tous les threads actifs sont assez fines pour que l'utilisateur ait l'impression que tous les threads s'exécutent en même temps[t1].
class ThreadTest implements Runnable { java.lang.String s; ThreadTest(String s) { this.s = s; } public void run() { while (true) System.out.println(s); } } public class TicTac { public static void main(String argv[]) { ThreadTest tic = new ThreadTest("TIC"); ThreadTest tac = new ThreadTest("TAC"); Thread ThrTic = new Thread(tic); Thread ThrTac = new Thread(tac); ThrTic.start(); ThrTac.start(); } }
Une version bien plus agréable de ce programme peut être écrite en remarquant qu'il faut systématiquement créer un thread et le démarrer à chaque création d'un objet ThreadTest. Il est alors tout à fait judicieux ceci fasse partie du constructeur de la classe ThreadTest.
class ThreadTest implements Runnable { java.lang.String s; Thread t ; ThreadTest(String s) { this.s = s; t = new Thread(this) ; t.start() ; } public void run() { while (true) System.out.println(s); } } public class TicTac { public static void main(String argv[]) { ThreadTest tic = new ThreadTest("TIC"); ThreadTest tac = new ThreadTest("TAC"); } }
Voilà un programme qui devrait, à priori, afficher des TIC et des TAC. Si vous exécutez ce programme et selon le système d'exploitation que vous utilisez, vous aurez la désagréable surprise de ne voir que des TIC et jamais des TAC ! Est-ce à dire que la programmation avec les threads n'est pas portable ? Oui, un peu mais nous verrons plus loin pourquoi il en est ainsi et comment, malgré tout, arriver à écrire des programmes portables avec les threads. En attendant, nous allons modifier ce programme pour qu'il fasse ce que l'on attend de lui.
Il existe des situations où il est utile de suspendre provisoirement un thread : c'est, par exemple, le cas des animations graphique que l'on voit sur les applets. Imaginons qu'une page HTML contienne une applet qui affiche une animation graphique. Celle-ci n'a pas besoin de s'exécuter lorsque la partie de la page HTML où apparaît l'applet n'est plus visible temporairement. Une solution simple pour ne pas effectuer les opérations inutiles d'affichages de l'animation consiste à détruire le thread qui gère cette animation et à la recréer une fois que cette partie de la page redevient visible. Ces opérations de destruction et de création des threads peuvent être coûteuses : une solution bien plus efficace consiste à suspendre l'animation le temps que l'animation sorte du champ de vision de l'utilisateur et à reprendre son exécution quand nécessaire. Pour ce faire, on dispose des méthodes suspend et resume de la classe Thread.
L'utilisation de la méthode suspend pour endormir un thread doit se faire avec précaution : il faut s'assurer qu'on finira par invoquer la méthode resume ou stop pour rendre actif ou pour tuer ce thread.
Une autre manière de suspendre un thread consiste à l'endormir pendant une certain durée. Encore une fois, si l'on réalise une animation graphique, pour éviter que les différentes images qui constituent l'animation ne s'affichent de manière trop rapide, on utilisera la méthode sleep pour suspendre l'exécution pendant un laps de temps que l'on donnera en argument de cette méthode. Cet argument correspond au nombre de millisecondes durant lesquelles on veut suspendre l'exécution. La méthode sleep lance l'exception InterruptedException si le thread est stoppé pendant son sommeil. L'invocation de la méthode sleep doit donc gérer cette exception.
try { monThread.sleep(500) ; } catch (InterruptedException e) { ... }
Il existe, en fait, deux méthodes sleep dans la classe Thread. La première prend en argument un entier de type long donnant la durée en milli secondes du temps d'interruption. Quant à la deuxième, elle prend un argument supplémentaire (de type int) ; la durée de suspension est alors augmentée d'un nombre de nano secondes donné par ce dernier argument.
public static native void sleep(long ms) throws InterruptedException public static void sleep(long ms, int ns) throws InterruptedException
On va, à présent, modifier l'exemple précédant pour réussir à faire exécuter en parallèle les deux threads. Puisque le premier thread qui s'exécute ne fait aucun cas de celui en attente d'exécution, on va forcer chacun de ces threads à s'endormir régulièrement de manière à permettre à l'autre de s'exécuter un peu. La méthode sleep va nous permettre de réaliser cela :
class ThreadTest implements Runnable { java.lang.String s; Thread t ; ThreadTest(String s) { this.s = s; t = new Thread(this) ; t.start() ; } public void run() { while (true) { System.out.println(s); try { t.sleep(100) ; } catch (InterruptedException e) { } } } }
Avec cette modification, on voit enfin apparaître les TIC et Les TAC entrelacés ! Preuve que ces deux threads s'exécutent bien en parallèle.
L'interface Runnable permet à tout objet d'une classe qui l'implantant d'être la cible d'un thread. Une autre manière de lancer un thread est de définir une sous classe de la classe Thread. En fait, la classe Thread implante elle-même l'interface Runnable. La méthode qu'elle définit est une méthode run vide. En redéfinissant la méthode run dans la sous classe, il est possible de définir les actions que l'on veut faire exécuter à notre thread.
class ThreadTest extends Thread { java.lang.String s; ThreadTest(String s) { this.s = s; } public void run() { while (true) { System.out.println(s); try { this.sleep(100) ; } catch (InterruptedException e) { } } } } public class TicTac { public static void main(String argv[]) { ThreadTest tic = new ThreadTest("TIC"); ThreadTest tac = new ThreadTest("TAC"); tic.start(); tac.start(); } }
La méthode start de la classe Thread invoque immédiatement, après initialisation du thread, la méthode run. En redéfinissant cette dernière dans la classe dérivée, on obtient le même résultat qu'avec le programme donné en 20.2.4 ci-dessus[t2].
Comme nous l'avons déjà dit, les threads partagent quasiment toutes les données entre eux. L'utilisation des threads est bien plus simple et efficace que l'utilisation des processus. Le prix à payer pour cette simplicité et cette efficacité est que le programmeur est tenu d'être vigilant dans l'utilisation de ces objets partagés. C'est à lui qu'incombe la responsabilité de la synchronisation des différents threads.
Le mot est lâché : synchronisation ! Partager des données entre différents threads impliquent un minimum d'entente entre les threads pour garantir l'intégrité des données manipulées. Nous verrons plus loin, dans ce chapitre, pourquoi et comment on peut réaliser cette synchronisation. Imaginons que deux thread exécutent la même séquence d'instructions suivante :
int a ; a = d.quelAnnée() ; a = a + 1 ; d.affecterAnnée(d) ;
La variable a est une variable locale et existe donc en deux exemplaires, un pour chaque thread. Si l'objet d n'est pas un objet locale, il est alors partagé par les deux threads. Chaque thread récupère dans la variable a la valeur du champ année de l'objet d. Puis, cette valeur est incrémentée de 1 et est rangée dans le champ année de l'objet d. Si ces threads s'exécutent en ``même temps'', la valeur rangée dans le champ année, après exécution des deux threads, ne sera incrémentée que de 1, alors que l'on s'attend à ce qu'il soit incrémenté de 2 (incrémenté de 1, une fois par chaque thread).
Pourquoi en est-il ainsi ? Un thread peut être interrompu à tout moment pour permettre à un autre thread de pouvoir s'exécuter. Il est tout à fait possible que les threads exécutant la séquence d'instructions donnée plus haut soit interrompus entre l'instruction d'incrément et celle du stockage de la valeur dans le champ année.
THREAD 1 THREAD 1 a = d.quelAnnée() ; Suspendu a = a + 1 ; Suspendu Suspendu a = d.quelAnnée() ; Suspendu a = a + 1 ; d.affecterAnnée(d) ; Suspendu Suspendu d.affecterAnnée(d) ; ... ...
La solution à ce problème consiste à interdire l'exécution de ce bout de code par plusieurs threads simultanément. En fait, Java ne permet pas de protéger n'importe quel partie du code contre les exécutions concurrentes. Il offre uniquement la possibilité de verrouiller un objet (pas une variable de type primitif) pour empêcher les accès concurrents. Lorsqu'une méthode qualifiée de synchronized est invoquée sur un objet par un thread, ce dernier est interdit pour toutes les méthodes qualifiées de synchronized s'appliquant sur le même objet par d'autres threads.
Imaginons que l'on dispose d'un unique mégaphone et de plusieurs orateurs qui veulent s'exprimer ; il va bien falloir coordonner la prise de parole de chaque orateur. Lorsqu'un orateur prend la parole, on attend qu'il ait fini pour passer la parole au suivant.
class MegaPhone { synchronized void parler(String qui, String dit, Thread t) { for (int i = 0 ; i<=10 ; i++ ) { System.out.println(qui + " affirme : "+ dit) ; try { t.sleep(200); } catch (InterruptedException e) {}; } } } class Orateur extends Thread { String nom, discours ; MegaPhone mph; public Orateur(String n, String d, MegaPhone m) { nom = n; discours = d ; mph = m; start(); } public void run() { mph.parler(nom, discours, this); } } class Assemblee { public static void main(String args[]) { MegaPhone mph = new MegaPhone() ; new Orateur("Orateur 1", "Je suis 1", mph) ; new Orateur("Orateur 2", "Je suis 2", mph) ; new Orateur("Orateur 3", "Je suis 3", mph) ; } }
Dans cet exemple, les trois orateurs utilisent un même mégaphone. Chaque orateur est constitué par un thread. La méthode parler est qualifiée de synchronized. Il en découle que l'objet de la classe MegaPhone est verrouillée dès lors qu'il est pris par un orateur même si celui-ci ne l'utilise pas à ``plein temps'' (sleep). Les autres orateurs ne pourront parler que lorsque celui qui détient le mégaphone a fini.
Une méthode synchronized pose un verrou sur l'objet sur lequel elle s'applique. Une méthode statique (méthodes de classes) peut elle être qualifiée de synchronized ? Eh bien, oui ! Les méthodes synchronized statiques pose un verrou sur la classe plutôt que sur un objet. Deux méthodes statiques et synchronized d'une même classe ne s'exécuter en même temps. En cela, ces méthodes sont proches des méthodes d'instance synchronized.
Par contre, il n'y aucun lien entre les verrous de class e et les verrous des objets . Une classe verrouillée (par une méthode synchronized static) n'empêche pas l'exécution d'une méthode d'instance synchronized et inversement. Les verrous de classe (resp. d'instance) ne synchronisent que les méthodes de classes (resp. d'instance)
A TERMINER
Avec le mot clé synchronized, nous avons pu verrouiller un objet sur tout une méthode. Il existe une instruction synchronized qui permet de verrouiller une partie d'une méthode. Cette instruction est constitué de deux parties : l'objet à verrouiller et le ou les instructions à exécuter.
synchronized (objet) instruction-simple-ou-bloc-d-instructions
En reprenant l'exemple de la modification de la date, on pourrait écrire le code suivant :
synchronized (d) { a = d.quelAnnée() ; a = a + 1 ; d.affecterAnnée(d) ; }
Une méthode qualifiée de synchronized est équivalente à la méthode non synchronized et composée de l'instruction synchronized.
synchronized type méthode (...) { corps de la méthode } type méthode (...) { synchronized (this) { corps de la méthode } }
Les méthodes et instructions synchronized permettent de contrôler l'interférence entre les threads. Nous allons à présent voir comment faire coopérer divers threads.
La méthode wait de la classe thread suspend l'exécution d'un thread en attendant qu'une certaine condition se réalise. La réalisation de cette condition est signalé par un autre thread par la méthode notify ou notifyAll. Toutes ces méthodes font partie de la classe Object et donc disponible par héritage à tous les classes.
Une méthode peut décider de s'interrompre pour laisser la place à un autre thread de s'exécuter. La méthode yield est celle qui permet à un thread de ne pas être égoïste. Reprenons l'exemple ci-dessus : nous avons dit que, selon l'architecture du système utilisée, il était possible que les threads s'exécutent de manière séquentielle et non concurrente. Il a fallu ajouter des appels à la méthode sleep pour suspendre, pendant une durée fixe, le thread en cours d'exécution. Cette solution simple n'est évidemment pas très agréable puisqu'il faut décider d'une durée de sommeil arbitraire alors que l'on aurait voulu que les ressources soient partagées au mieux. Avec cette approche, les deux threads que l'on voulait exécuter pouvaient être en sommeil en même temps et les ressources CPU sont perdus pour tout le monde.
La méthode yield indique que le thread en cours d'exécution accepte de s'interrompre pour laisser la possibilité aux autres threads de s'exécuter. Le temps de sommeil sera laissé à l'appréciation du scheduler .
class ThreadTest implements Runnable { java.lang.String s; ThreadTest(String s) { this.s = s; } public void run() { while (true) { System.out.println(s); yield() ; } } }
Avec cette modification, on devrait obtenir le même résultat qu'avec l'exemple de la section 20.2.4 avec toutefois une meilleur répartition du temps CPU.
try { UnThread.join(500) ; } catch (InterruptedException e) { ... }
Il existe, en fait, trois méthodes join dans la classe Thread. La première est sans argument et attend la fin de l'exécution du thread sur lequel cette méthode s'applique. La deuxième prend en argument un entier de type long donnant la durée en milli secondes du temps d'attente maximale. Si cet argument vaut 0, cette méthode est équivalent à la première forme. Quant à la troisième, elle prend un argument supplémentaire (de type int) ; la durée d'attente est alors augmentée d'un nombre de nano secondes donné par ce dernier argument.
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 InterruptedException
Illustrons ceci avec l'exemple de la section 20.2.4 ci-dessus et ajoutons un nouvel orateur qui doit clôturer la séance. Il faut qu'il attende que tous les orateurs se soient exprimés pour prononcer son allocution de fin de séance.
class Assemblee { public static void main(String args[]) { MegaPhone mph = new MegaPhone() ; Orateur o1 = new Orateur("Orateur 1", "Je suis 1", mph) ; Orateur o2 = new Orateur("Orateur 2", "Je suis 2", mph) ; Orateur o3 = new Orateur("Orateur 3", "Je suis 3", mph) ; try { o1.join(); o2.join() ; o3.join(); } catch (InterruptedException e) {}; new Orateur("Président", "Je suis le président et je clotûre la séance", mph) ; } }
Lorsque la méthode wait est invoquée à partir d'une méthode synchronized, en même temps que l'exécution est suspendue, le verrou posé sur l'objet à partir de laquelle cette méthode à été invoquée est relâché. Dès que la condition de réveil survient, le thread attend de pouvoir reprendre le verrou et continuer l'exécution.
public final void wait() throws InterruptedException
Une autre version de wait prend en argument un entier de type long. Cet argument définit la durée d'attente maximale (en millisecondes)de la survenue de l'évènement. Lorsque ce temps d'attente est dépassée, même si la condition espérée ne survient pas.
public final native void wait(long ms) throws InterruptedException
Une dernière version de wait prend en argument un entier de type long et un entier de type int. Dans ce cas, la durée d'attente est de ms millisecondes plus ns nanosecondes.
public final void wait(long ms, int ns) throws InterruptedException
Une dernière version de wait prend en argument un entier de type long et un entier de type int. Dans ce cas, la durée d'attente est de ms millisecondes plus ns nanosecondes.
La méthode notify réveille un et un seul thread qui est attente. Si plusieurs threads sont en attente, le thread réveillé est celui qui a été suspendu le plus longtemps. La notification n'est pas conditionnée par un évènement : les threads ne savent pas quelle condition à été satisfaite. C'est au code qui à invoqué la méthode wait de vérifier après coup s'il s'agit bien de la condition qu'il attendait. Tout ceci pour signaler que la mise en attente d'un thread doit tester la condition de réveil.
while ( ! condition) wait() ; ... // Code a exécuter quand la condition sera vraie
Lorsque plusieurs threads sont en attente et que l'on veut les réveiller tous, on utilisera la méthode notifyAll. Dans la plupart des cas, c'est plutôt cette méthode que l'on utilisera.
Notre assemblée est constitué d'orateurs ``sympas'' : au lieu d'accaparer le mégaphone jusqu'à la fin de leur discours, ils acceptent d'interrompre leur discours de temps en temps, de libérer le mégaphone et de se mettre en attente de la disponibilité du mégaphone.
class MegaPhone { boolean libre = true; synchronized void parler(String qui, String dit, Thread t) { for (int i = 0 ; i<=10 ; i++ ) { System.out.println(qui + " affirme : "+ dit) ; notifyAll() ; // Libère le mégaphone try { if (! libre) wait() ; } catch (InterruptedException e) {};// se met en attente du mégaphone } } }
On remarquera que cette méthode est qualifiée de synchronized et le verrou posé est libéré lors de l'appel de wait pour permettre l'exécution des autres threads.
Supposons à présent que, suite à des conflits internes, une scission de cette assemblée se produit : les protagonistes se retrouvent à présent avec deux mégaphones.
class LesMegaPhones { boolean estLibre[]={true, true};
Et malgré le conflit, les orateurs reste courtois et se partagent les mégaphones. La méthode disponible donne ne numéro du mégaphone libre. Si les deux mégaphones sont pris, elle retourne -1.
synchronized int disponible() { for (int i=0; i<estLibre.length; i++) if (estLibre[i]) return i; return -1; }
Un orateur qui vent prendre possession d'un mégaphone, recherche un mégaphone libre. Si aucun n'est disponible, il se met en attente.
synchronized int accaparer() { int i; while ((i =disponible()) == -1 ) { try { wait(); } catch (InterruptedException e) {}; } estLibre[i]=false; return i; }
Lorsque l'orateur a fini de s'exprimer, il libère le mégaphone et le signale à tous les autres orateurs.
synchronized void liberer(int i) { estLibre[i]=true; notifyAll(); } }
Comme dans l'exemple précédent, la classe Orateur est une classe dérivée de la classe Thread.
class Orateur extends Thread { String nom, discours ; LesMegaPhones m; public Orateur(String n, String d, LesMegaPhones m) { nom = n; discours = d ; this.m = m; start(); } public void run() { parler(); }
Pour parler, l'orateur prend un mégaphone (si possible), dit ce qu'il a dire, libère le mégaphone et répète ces opérations plusieurs fois.
synchronized void parler() { int numMega; for (int j = 0; j < 5; j++) { numMega = m.accaparer(); // prend un mégaphone ou se met en attente try {sleep(1000); } catch (InterruptedException e) {}; System.out.println(nom + " affirme avec le megaphone " + numMega + " " + discours) ; m.liberer(numMega); // libère le mégaphone } } }
Et enfin, la classe principale :
class Assemblee { public static void main(String args[]) { LesMegaPhones mph = new LesMegaPhones(); new Orateur("Orateur 1", "Je suis 1", mph) ; new Orateur("Orateur 2", "Je suis 2", mph) ; new Orateur("Orateur 3", "Je suis 3", mph) ; new Orateur("Orateur 4", "Je suis 4", mph) ; new Orateur("Orateur 5", "Je suis 5", mph) ; } }
Lorsqu'une application Java démarre, un premier thread s'exécute : il s'agit du thread principal, celui qui démarre l'exécution de la méthode main dans le cas des applications autonomes. Lorsque la méthode main se termine et si aucun autre thread n'a été crée, l'application s'arrête. Par contre, si des threads ont été crées, même si la méthode main arrive à la fin de son exécution, l'application ne s'arrêtera que lorsque tous les autres threads arriveront en fin d'exécution. La fin de l'exécution d'un thread survient lorsque toutes les instructions de la méthode run correspondante ont été exécutées. Que se passe-t-il si la méthode run est conçue pour fonctionner indéfiniment, comme c'est le cas, par exemple, pour les animations graphiques ? Notre thread continue à s'exécuter indéfiniment : et ce même si le code l'ayant lancer est terminé. Un programme Java ne se termine que lorsque tous les threads sont terminés. Un thread qui s'exécute indéfiniment force donc l'interpréteur Java à ne pas s'arrêter. Les threads orphelins continuent à mobiliser des ressources système et l'interpréteur.
Comment donc arrêter les threads en cours (actifs ou endormis) ? Il y plusieurs méthodes :
void run() { while ( ! stoperLesThreads) { ... } }
class MegaPhone { synchronized void parler(String qui, String dit, Thread t) { for (int i = 0 ; i<=1 ; i++ ) { System.out.println(qui + " affirme : "+ dit) ; try { t.sleep(200); } catch (InterruptedException e) {}; } } } class Orateur extends Thread { String nom, discours ; MegaPhone mph; public Orateur(String n, String d, MegaPhone m) { nom = n; discours = d ; mph = m; start(); } public void run() { for (int i = 0 ; i<=1 ; i++ ) { mph.parler(nom, discours, this); yield(); } } } class AssembleeJoin { public static void main(String args[]) { MegaPhone mph = new MegaPhone() ; Orateur o1 = new Orateur("Orateur 1", "Je suis 1", mph) ; Orateur o2 = new Orateur("Orateur 2", "Je suis 2", mph) ; Orateur o3 = new Orateur("Orateur 3", "Je suis 3", mph) ; try { o1.join(200); o2.join(200); o3.join(200); } catch (InterruptedException e) {}; System.out.println("Je suis le président et je suis préssé") ; o1.stop(); o2.stop(); o3.stop(); new Orateur("Président", "Je suis le président et je clotûre la séance", mph) ; } }
Comme nous l'avons déjà vu, les applets sont des applications qui sont capables de démarrer et de s'arrêter en fonction de l'action de l'utilisateur. Les méthodes init, start et stop permettent respectivement d'initialiser, de démarrer et d'arrêter. Contrairement aux threads, les applets sont susceptibles d'être démarrés et arrêtés autant de fois que nécessaire : le concept de suspension n'existe pas pour les applets. On peut calquer le comportement des threads que l'on veut gérer dans une applet avec celui de l'applet elle-même :
A titre d'exemple, créons une applet qui affiche toutes les secondes l'heure. Il s'agit tout d'abord de définir une classe dérivée de classe java.applet.Applet et qui implente l'interface Runnable. Cette classe possède pour champ privé un objet de la classe Thread.
public class clockApp extends java.applet.Applet implements Runnable { private Thread t; ... }
Cette applet crée un nouveau thread à chaque fois que l'applet démarre et le détruit lorsque celle-ci s'arrête. On invoque la méthode stop sur ce thread et on affecte à null la variable qui référence ce thread pour que Java récupère la mémoire utilisée pour celui-ci.
public void start() { t = new Thread(this); t.start(); } public void stop() { t.stop(); t = null; }
Même si cela ne justifie pas ici, une bonne habitude de programmation lors de la programmation des threads consiste à tester les valeurs des threads avant leur utilisation : pour éviter de créer par inadvertance plusieurs threads inutilement ou tenter de détruire des threads inexistants :
public void start() { if (t == null) { t = new Thread(this); t.start(); } } public void stop() { if (t != null) { t.stop(); t = null; } }
Le filet d'exécution confié au thread associé à cette applet consiste tout simplement à s'endormir pendant une seconde puis à invoquer la méthode repaint pour rafraîchir l'affichage de l'applet.
public void run() { while (true) { try { t.sleep(1000); } catch (InterruptedException e) {} repaint(); } }
La méthode repaint de l'applet se contente d'afficher la date donnée par le système.
public void paint(java.awt.Graphics g) { g.drawString(new java.util.Date().toString(), 10, 20); } }
Une fois compilé et créé le fichier HTML qui fait appel à cette classe clockApp, on devrait voir s'égrener les secondes sur notre page HTML.
<HTML> <HEAD> <TITLE>Et le temps passe !!!</TITLE> </HEAD> <BODY> ... <APPLET CODE="clockApp.class" WIDTH=250 HEIGHT=25> </APPLET> ... </BODY> </HTML>
Dans cet exemple, le thread est systématiquement détruit chaque fois que l'applet invoque la méthode stop ce qui implique que l'on est obligé de récréer un nouveau thread avec la méthode start. En imaginant que l'initialisation de notre thread implique des opérations coûteuses, il serait bien plus judicieux de suspendre le thread. Le problème qui se pose alors est de déterminer le moment où l'on détruira effectivement ce thread. Le comportement d'utilisateur devant sa page HTML n'étant pas prévisible, on ne pourra jamais être assuré de pouvoir détruire le thread. A moins que l'on remarque que la méthode destroy de la classe Applet est invoquée lorsque l'applet doit être détruite entièrement (généralement lors de sa disparition du cache). Cette méthode permet donc de libérer toutes les ressources mobilisées par une applet.
Grâce à cette méthode destroy, nous allons pouvoir suspendre et reprendre l'exécution du thread lorsque l'applet s'arrête et redémarre ; et on laissera le soin à la méthode destroy de tuer le thread.
public void start() { if (t == null) { t = new Thread(this); t.start(); } else t.resume() ; } public void stop() { if (t != null) t.suspend(); } public void destroy() { t.stop(); t = null; }
Chaque thread appartient à un groupe de threads. Par défaut, les threads que l'on crée (sauf contre indication) font partie du même groupe que le thread qui l'a créé. Au démarrage d'une application Java , il existe un premier groupe de thread main qui contient le thread principal et le thread qui se charge de la récupération de mémoire.
Pour créer un nouveau thread appartenant un nouveau groupe de threads, on crée tout d'abord le groupe de thread qui est un objet de la classe ThreadGroup et ensuite on crée le thread en utilisant une des constructeurs qui prend en argument un objet de la classe ThreadGroup.
ThreadGroup monGroupe = new ThreadGroup("Mon groupe de threads") ; Thread t = new Thread(monGroupe) ;
Les goupes de threads sont constitués de threads et d'autres groupes de threads. Du groupe principal appelé main, on peut alors créer une arborescence complète de threads et de groupes de threads. L'orgranisation des threads en groupes pour des raisons
Collection Management Methods
La classe ThreadGroup fournit un ensemble de méthodes pour gérer les threads et les sous groupes de threads ainsi que pour obtenir les caractéritiques des groupes de threads. Par exemple, il existe une méthode activeCount qui donne le nombre de threads actifs dans un groupe. Il existe également une méthode enumerate qui donne la liste des threads actifs dans un groupe. Voici un exemple de programme qui remplit un tableau de threads actifs dans le groupe de thread courant.
class ListeDeThreads { void listeDesThreadsCourants() { ThreadGroup groupeCourant = Thread.currentThread().getThreadGroup(); int nbreThreads; Thread[] listeDeThreads; nbreThreads = groupCourant.activeCount(); listeDeThreads = new Thread[nbreThreads]; GroupCourant.enumerate(listeDeThreads); for (int i = 0; i < numThreads; i++) { System.out.println("Thread numéro " + i + " = " + listeDeThreads[i].getName()); } } }
A FINIR
Comme nous l'avons déjà dit, les threads ne s'exécutent pas forcément en parallèle. En pratique, en particulier sur les machines mono processeur, chaque thread s'exécutent tout seul. C'est la répartition du temps CPU à chaque thread qui nous donne l'illusion qu'ils s'exécutent tous en même temps. Cette technique de distribution des ressources aux threads (ou processus) s'appelle l'ordonnancement (scheduling). Le langage Java dispose d'un algorithme déterministe simple basé sur la notion de priorité des threads.
A chaque thread est associé une priorité. Une priorité est un entier compris entre les valeurs MIN_PRIORITY and MAX_PRIORITY définies dans la classe Thread. Lorsque plusieurs threads sont démarrés, celui qui a la priorité la plus forte s'exécute en premier. C'est uniquement lorsque les threads de priorité le plus élevé ont fini leur exécution ou qu'ils sont suspendus pour une raison quelconque que les autres threads sont exécutés. S'il existe deux threads de même priorité en attente d'exécution, java choisit un des threads et l'exécute jusqu'à ce que l'un des évènements suivants se produisent :
L'algorithme d'ordonnancement de Java est préemptif : a tout moment, Java peut interrompre un thread en cous d'exécution pour donner la main à un thread de priorité plus forte.
class Président extends Orateur { private Orateurs [] listeOrateurs ; Président(String n, String d, MégaPhone m, Orateurs [] o) { super(n, d, m) ; listeOrateurs = o ; setPriority(MAX_PRIORITY) ; } public void run() { for (int i = 0 ; i < o.length ; i++) o[i].join(100) ; for (int i = 0 ; i<= 3 ; i++) { mph.parler(nom, discours, this); try { t.sleep(20); } catch (InterruptedException e) {} } for (int i = 0 ; i < o.length ; i++) o[i].stop(100) ; } } class Assemblée { public static void main(String args[]) { MegaPhone mph = new MegaPhone() ; Orateur [] lesOrateur = { new Orateur("Orateur 1", "Je suis 1", mph), new Orateur("Orateur 2", "Je suis 2", mph), new Orateur("Orateur 3", "Je suis 3", mph) } ; new Président("Président", "Je suis le président et je clotûre la séance", mph, lesOrateurs) ; } }
Certains systèmes comme 95/NT , implante la technique du time-slicing pour lutter contre les threads égoïstes qui accaparent les ressources CPU. Le time-slicing n'a d'intérêt que lorsque plusieurs threads de même priorité sont en activité. C'était le cas de notre exemple de la section 20.2.4 des threads TIC et TAC. Dans les systèmes ne possédant pas le time-slicing , cet exemple produit une succession de TIC et comme il s'agit d'une boucle infini, on aura jamais aucun TAC à l'écran. En effet, dans de tels systèmes, une fois un thread choisi pour être exécuté, celui conserve cette ressource jusqu'à ce que
Si l'on veut réaliser des applications portable, il ne faut pas utiliser les possibilités du time-slicing .
A TERMINER
Ce comportement peut paraître curieux mais c'est ainsi que l'on peut réaliser des démons (daemons, en anglais). Les daemons sont des programmes qui s'exécutent périodiquement en arrière plan.
Any Java thread can be a daemon thread. Daemon threads are service providers for other threads running in the same process as the daemon thread. For example, the HotJava browser uses up to four daemon threads named "Image Fetcher" to fetch images from the file system or network for any thread that needs one. The run() method for a daemon thread is typically an infinite loop that waits for a service request.
When the only remaining threads in a process are daemon threads, the interpreter exits. This makes sense because when only daemon threads remain, there is no other thread for which a daemon thread can provide a service.
To specify that a thread is a daemon thread, call the setDaemon method with the argument true. To determine if a thread is a daemon thread, use the accessor method isDaemon.
public class Thread implements Runnable { public final static int MIN_PRIORITY = 1; public final static int MAX_PRIORITY = 10; public final static int NORM_PRIORITY = 5; public Thread(); public Thread(String name); public Thread(Runnable target); public Thread(Runnable target, String name); public Thread(ThreadGroup group, String name) throws SecurityException, IllegalThreadStateException; public Thread(ThreadGroup group, Runnable target) throws SecurityException, IllegalThreadStateException; public Thread(ThreadGroup group, Runnable target,String name) throws SecurityException, IllegalThreadStateException; public String toString(); public void checkAccess(); public void run(); public void start() throws IllegalThreadStateException; public final void stop() throws SecurityException; public final void stop(Throwable o) throws SecurityException; public final void suspend() throws SecurityException; public final void resume() throws SecurityException; public void destroy); public final boolean isAlive); public void interrupt); public static boolean interrupted); public boolean isInterrupted); public final String getName); public final void setNameString name) throws SecurityException; public final ThreadGroup getThreadGroup); public final int getPriority); public final void setPriorityint newPriority) throws SecurityException, IllegalArgumentException; public final boolean isDaemon); public final void setDaemonboolean on) throws SecurityException; public int countStackFrames); public final void join) throws InterruptedException; public final void joinlong millis) throws InterruptedException; public final void joinlong millis, int nanos) throws InterruptedException; public void interrupt); public boolean isInterrupted); public static boolean interrupted); public static Thread currentThread); public static int activeCount) // deprecated public static int enumerateThread tarray[]); // deprecated public static void dumpStack); public static void yield); public static void sleeplong millis) throws InterruptedException; public static void sleeplong millis, int nanos) throws InterruptedException; }
public class ThreadGroup { public ThreadGroupString name) throws SecurityException; public ThreadGroupThreadGroup parent, String name) throws NullPointerExpression, SecurityException, IllegalThreadStateException; public String toString); public final void checkAccess); public final String getName); public final ThreadGroup getParent); public final boolean parentOfThreadGroup g); public final void stop) throws SecurityException; public final void suspend) throws SecurityException; public final void resume) throws SecurityException; public final void destroy) throws SecurityException, IllegalThreadStateException; public final int getMaxPriority); public final void setMaxPriorityint newMaxPriority) throws SecurityException, IllegalArgumentException; public final boolean isDaemon); public final void setDaemonboolean daemon) throws SecurityException; public int threadsCount); public int allThreadsCount) ; public int groupsCount); public int allGroupsCount); public Thread[] threads); public ThreadGroup[] groups); public Thread[] allThreads); public ThreadGroup[] allGroups); public int activeCount); // deprecated public int activeGroupCount); // deprecated public int enumerateThread list[]); // deprecated public int enumerateThread list[], // deprecated boolean recurse); public int enumerateThreadGroup list[]); // deprecated public int enumerateThreadGroup list[], // deprecated boolean recurse); public void list); public void uncaughtExceptionThread t, Throwable e); }
[t1]Ceci n'est pas complètement exact : il y a le cas particulier des thread démons (daemons, en anglais) que nous verrons plus loin.
[t2] Edpliquer pourquoi on fait toujours comme ça
[t3] Expliquer Mieux