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:
-
Deux processus ne modifient/lisent pas l'objet simultanément.
- L'objet est bien lu après avoir été écrit.
- 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.