MIM1
TD n
°8 de POOGL
Threads en JAVA

Vincent BOUDET
Bureau 343 (84-70)
email vboudet@ens-lyon.fr

5 Avril 2001

A   A la découverte des threads

Pour créer une classe de processus léger (thread) Fred, il y a deux possibilités: 1) Fred hérite de la classe Thread; 2) Fred implémente l'interface Runnable et est passé en argument au constructeur Thread pour son exécution. Le thread Fred sera lancé par l'appel à la méthode Fred.start(), responsable de l'initialisation du processus léger et qui appelera la méthode Fred.run(). Un processus léger se termine par un appel à sa méthode stop().

A.1   Un premier exemple

Question A.1   Selon vous, quand doit-on utiliser l'interface Runnable au lieu du simple héritage de la classe Thread?
Question A.2   Ecrire un thread qui affiche 10 fois une chaine donnée (à son constructeur) et attend un temps aléatoire de 0 à 1 seconde entre deux affichages. (la méthode sleep(int) de la classe Thread permet d'attendre un temps donné en ms)
Question A.3   Ecrire un programme qui lance deux de ces threads et les exécute en parallèle. Exécutez-le plusieurs fois.
Question A.4   Remplacer l'appel à la méthode sleep(int) par une boucle d'attente. Que constatez-vous?
Afin d'éviter qu'un thread ne garde la main, il est possible d'interrompre volontairement son exécution: la méthode yield() permet d'interrompre le thread pour permettre à un autre thread (de même priorité, cf. plus loin) de reprendre son exécution.
Question A.5   Utiliser cette méthode pour permettre aux deux threads de s'exécuter en parallèle.

A.2   Priorités

En Java, les thread peuvent avoir des priorités différentes. C'est le thread de priorité maximale qui est systématiquement exécuté. Les méthodes setPriority et getPriority permettent de modifier les priorités des processus.
Question A.6   Modifier les priorités des deux threads lancés par le programme précédent. Que constatez-vous?

A.3   Threads et Applets

Une applette exécute lors de son premier lancement sa méthode init() puis à chaque redémarrage sa méthode start(). En particulier la méthode start() est rappelée à chaque fois que la page web est rechargée. Une applette controle son affichage avec les méthodes paint() et repaint(). Un thread est lancé par l'éxecution de sa méthode start() (qui ne doit jamais être redéfinie) qui lance sa méthode run() qui contient le corps de son programme. Sa méthode stop() est exécutée à la fin de sa vie. La méthode isAlive() permet de connaître l'avancement de la vie du thread: entre sa création et l'appel à sa méthode start(), ainsi qu'après sa mort, un thread est dans l'état isAlive()==false; entre son lancement et la fin de son exécution (incluant les périodes d'endormissement), il est dans l'état isAlive()==true. Il est donc possible d'hériter à la fois de Applet et d'implémenter l'interface Runnable afin de pouvoir transformer une applette en thread pour paralléliser la gestion de son exécution, en particulier le raffraichissement de son écran. Il suffit pour cela de lui définir: les méthodes init() et start() de l'applette, et les méthodes run() et stop() du thread. Imaginons que nous ayant une applette FredLApplette qui implémente l'interface Runnable et donc en particulier les méthodes start() et run(). Une telle applette peut créer un thread à partir d'elle-même dans sa méthode start() (en faisant new Thread(this) par exemple). Ce thread exécutera la méthode run() et aura accès aux méthodes et champs propres de l'applette (telles repaint(),...) et à ceux et celles du thread (telles wait(), sleep()). Ceci permet en particulier de gérer le raffraîchissement de l'écran à intervalle de temps régulier (par ex: tous les secondes avec un sleep(1000) entre deux repaint()).
Question A.7   Ecrire une applette qui déplace un rond rouge de gauche à droite toutes les 100ms.
Question A.8   Ecrire une applette qui lance une course entre deux compteurs allant de 0 à 10 000 000 (des threads) qui seront représentés par des traits. Faites varier les priorités entre les deux compteurs.
Question A.9   Faites en sorte que la ``course'' reprenne à zéro à chaque rechargement de la page web.

B   Synchronisation de threads

Lorsque plusieurs threads mainpulent un objet commun, il faut pouvoir garantir qu'il ne se passe pas n'importe quoi. Pour cela, on construit un système de verrous qui devra en particulier assurer que:
  1. Deux processus ne modifient/lisent pas l'objet simultanément.
  2. L'objet est bien lu après avoir été écrit.
  3. Que toutes les actions entreprises seront exécutées.
Le premier point est assuré à l'aide du mot-clé synchronized qui pose un verrou sur l'objet et ne permet qu'au premier processus à avoir appeler une méthode ``vérouillante'' d'accèder à l'objet. Le second point est assuré en utilisant les méthodes wait, notify, notifyAll. La méthode wait permet ``d'endormir'' le processus qui a posé un verrou sur l'objet (via une méthode synchronized), alors qu'il ne peut l'utiliser (ex: données non-présentes dans un objet tampon), et de lui faire relâcher le verrou; un autre processus peut alors accèder à l'objet (ex: et éventuellement écrire des données dans le tampon). Les méthodes notify et notifyAll permettent de ``réveiller'' les processus endormis par des appels à la méthode wait. Ceci se fait via l'envoi d'une InterruptedException. Aussi, tous les appels aux méthodes wait, ou sleep, doivent être faits dans un bloc try { ... } catch (InterruptedException e) { }. Le troisième point est assuré en essayant d'accèder à l'objet, jusqu'à ce que l'accès soit autorisé en jouant avec un while et la méthode wait().

B.1   L'exemple des Producteurs/Consommateurs

Question B.1   Ecrire une classe Boite, contenant un entier, et qui propose deux méthodes put(int) et get() qui permettent de mettre dans la boîte un entier dès que la boîte se vide, et de lire un entier et de le retirer de la boîte dès que la boîte est pleine.
Question B.2   Ecrire les classes Producteur et Consommateur qui implémentent deux threads: le premier écrit des entiers dans la boîte à intervalles de temps aléatoires; le second lit les entiers dès qu'ils sont disponibles.
Question B.3   Ecrire un programme principal qui génère tout d'abord un producteur, une boîte et un consommateur et lance leur exécution; puis des producteurs et des consommateurs, toujours avec une seule boîte (pensez à numéroter les producteurs et les consommateurs pour permettre un affichage).
Question B.4   Certifier que votre programme se comporte convenablement.

B.2   Exemple d'absence de synchronisation

Question B.5   Modifier la boîte précédente pour qu'elle contienne un tableau d'entiers au lieu d'un simple entier. Modifier le producteur pour qu'il remplisse le tableau comme un tampon avec les points d'une sinusoïde. Modifier le consommateur pour qu'il lise les points à afficher et les représentent dans un canvas (faites hériter le consommateur de canvas). Constatez que s'il on retire le motc-clé synchronized sur les méthodes de lecture et d'écriture de la boîte, on observe des sauts dans la courbe.

B.3   Le diner des philosophes

Cinq philosophes sont installés autour d'une table. Entre deux philosophes, il y a exactement une fourchette. Les philosophes mangent puis réfléchissent quelques instant, puis remangent, etc... Un philosophe peut manger lorsqu'il a les deux fourchettes dans ses mains. Un philosophe ne peut saisir qu'une fourchette à la fois.
Question B.6   Proposer un mécanisme de verrous et de demande de fourchettes sans blocage pour permettre aux philosophes de manger sans jamais bloquer.
Question B.7   Ecrire une applette qui implémente votre solution. (A priori il y a un thread par philosophe et un thread par fourchette,...)

This document was translated from LATEX by HEVEA.