TP2 : Threads Java
Téléchargez cette archive contenant
des exemples et des squelettes de programme qu'il faudra compléter
par la suite.
Exercice 0
Placez-vous dans le répertoire 'ExemplesDuCours'.
- Lancez le programme
ExempleConcurrent
plusieurs de
fois de suite. Constatez que le résultat n'est pas toujours
identique. Comprendre pourquoi.
- Lancez le programme
EvtGenerator
plusieurs de fois
de suite. Comprendre les appels à wait()
et
notifyall()
réalisés dans le code.
Exercice 1
Rendez-vous dans le répertoire 'ProdCons'.
Il y a dans ce système deux types de threads :
- un thread "producteur" qui produit périodiquement un objet (qui
correspond ici à ajouter un entier quelconque à la fin
d'un tableau)
- un thread "consommateur" qui consomme périodiquement un
objet (le dernier entier du tableau est supprimé)
Tous les threads communiquent par l'intermédiaire
d'un entrepôt global, de taille fixée, initialement vide.
Le problème consiste à synchroniser tous les threads en jeu,
de façon à ce que les contraintes suivantes soient
vérifiées :
- un thread producteur reste bloqué tant que l'entrepôt ne
dispose pas de place libre
- un thread consommateur reste bloqué tant que l'entrepôt ne
contient pas d'objet à consommer.
- Completez le squelette de programme
ProdCons.java
pour
qu'il ait les fonctionnalités décrites ci-dessus.
Rappels :
synchronized(obj)
: signifie qu'un seul thread
à la permission de rentrer dans le moniteur de obj
.
wait
: relache le moniteur et attend
l'émission d'un notify
ou notifyall
: à la sortie du wait
, le thread reprend le
moniteur.
notify
: réveille un thread en attente.
Attention : lorsque notify
est appelée et qu'il n'y a
aucun thread en attente, cette notification est perdue (elle ne sera pas
réémise).
notifyall
: réveille tous les threads en attente.
Exercice 2
Placez-vous dans le répertoire 'LecteurEcrivain'.
On se place ici dans le cadre d'un système disposant d'une
donnée partagée et comportant deux types de threads :
- un thread "lecteur" est un thread qui ne fait que consulter
l'état de la donnée
- un thread "ecrivain" est un thread qui modifie
l'état de la donnée.
Le problème de synchronisation est ici
défini par les contraintes suivantes :
- à tout moment, il peut y avoir plusieurs threads lecteurs
utilisant la donnée partagée (ils n'entrent pas en
conflit)
- à tout moment, il ne peut y avoir qu'un seul thread
écrivain utilisant la donnée partagée, et ce
thread y dispose d'un accès exclusif (c'est-à-dire qu'il
n'admet ni un autre écrivain, ni un autre lecteur).
Tel qu'il est défini, ce système peut se trouver dans une situation
dite "de famine". En effet, imaginons qu'il y ait beaucoup de threads
lecteurs, à un point tel qu'il y ait à tout moment au
moins un thread lecteur utilisant la donnée partagée.
Dans ce cas, un éventuel thread écrivain n'aura jamais
accès à la donnée : il sera en situation de famine.
(Notez que dans certains cas particuliers la situation est symétrique.
Si il y a "beaucoup" d'écrivains, les lecteurs risquent de rester à
l'état de famine. Toutefois, si l'attribution des accès
est équitable, cela ne devrait pas arriver.)
Nous allons ici choisir de régler ce problème
en énoncant une règle de priorité :
- à partir du moment où un écrivain a
demandé un accès exclusif à la donnée
globale (même si il ne peut pas l'obtenir immédiatement
pour cause de présence de lecteurs), aucun lecteur ne peut
obtenir d'accès à la donnée jusqu'à ce que
l'écrivain ait terminé son accès.
Pour comprendre l'influence de cette règle,
examinons un exemple impliquant 3 threads, deux lecteurs L1 et L2 et un
écrivain E1, dans la séquence suivante :
- L1 demande l'accès à la donnée :
accordé
- E1 demande l'accès à la donnée :
bloqué
- L2 demande l'accès à la donnée
Dans ce cas, la règle de priorité modifie le
comportement global. En effet :
- sans la règle de priorité, L2 obtient son
accès, et "dépasse" E1, qui risque la famine
- avec la règle de priorité, L2 est bloqué, et
E1 obtiendra l'accès lorsque L1 aura terminé le sien ; L2
quant à lui obtiendra l'accès après que E1 aura
fini le sien.
(Tout ceci suppose qu'il n'y a pas d'autre demande
d'accès entre temps.)
- Completez le squelette de programme
LecteurEcrivain.java
pour qu'il ait les fonctionnalités décrites ci-dessus.
- Imaginez une autre stratégie pour qu'il n'y ait plus de situation
de famine. Dans le système que vous proposez, un lecteur/écrivain
devra attendre uniquement les lecteurs/écrivains arrivés avant
lui. Il vous faudra donc gérer une file d'attente.
Exercice 3
Placez-vous dans le répertoire 'Tri'.
- Parallelisez l'algorithme de Tri du fichier
Tri.java
en utilisant
les threads. Avec des mesures de temps (utilisez par exemple la méthode
long System.currentTimeMillis()
qui renvoie le nombre de millisecondes
écoulées depuis le 01/01/1970) sur une grande liste d'entiers
(prévoyez un générateur automatique).
- Vous montrerez que votre solution est efficace sur une machine multiprocesseur :
actuellement, la machine 'codd' possède 4 processeurs et 'turing' en à 16.
[Page réalisée à à l'aide
d'un document de Guillaume
Latu]