Introduction
Qu'est ce qu'un thread ?
Système multi-tâches
(multi-thread) : exécution concourrante de plusieurs tâche
(Thread) sur un même processeur
Le système passe par un
ordonnanceur de tâches (scheduler) qui attribue des temps
d'exécution aux différentes
tâches à tour de
rôle selon une politique définie.
L'activation d'une
tâche est cyclique et dure un temps relativement bref, ce qui
donne l'illusion du parallélisme.
Généralement, une
application (java par exemple) correspond à un processus qui
lui
peut contenir un ou plusieurs threads.
Les threads en Java
Un programme Java (lancé via la
JVM) lance un processus de base qui contient plusieurs threads:
le thread principal (celui qui exécute le code
à partir du main) et d'autres pour, par exemple,
la gestion du
"garbage collector".
En java, il est possible de
développer
ses propres threads afin de permettre la réalisation de
plusieurs tâches en parallèle (de façon
concourrantes).
Création et démarrage d'un
thread.
Il existe, en Java, deux techniques pour
créer un thread.
Soit en dérivant un thread de la classe
java.lang.Thread,
Soit en implémentant l'interface
java.lang.Runnable.
Le choix de l'une ou l'autre des techniques dépend du contexte
de l'implémentation.
Par exemple, on choisit
d'implémenter l'interface plutôt que la dérivation
si notre
thread doit hériter d'une autre classe (Ex/ cas de la
classe montre vue en TD).
Créer un thread en dérivant de la classe
java.lang.Thread.
La dérivation de la classe
java.lang.Thread passe par au moins
1) la définition d'un
constructeur et
2) la surcharge de la méthode public void
run() héritée de java.lang.Thread.
public
class FirstThread extends Thread {
private String
threadName;
/** Un attribut propre
à chaque thread : nom du Thread */
public FirstThread(String
threadName) {
this.threadName = threadName;
/** start est la
méthode qui permet de lance
le thread (héritée de java.lang.Thread) **/
this.start();
---> ligne indispensable !!
}
/** Le but d'un tel thread est
d'afficher
500 fois
* son attribut threadName. Notons
que la
méthode
* <I>sleep</I> peut
déclancher des exceptions.
*/
public void run() {
try {
for(int i=0;i<500;i++) {
System.out.println( "Thread nommé : " + this.threadName +"
- itération : " + i);
/*** sleep est une
methode
de la classe java.lang.Thread , qui met en
veille le thread
pendant la
duréee
indiquée par le paramètre em
millisecondes***/
Thread.sleep(30);
}
} catch
(InterruptedException exc) {
exc.printStackTrace();
}
}
|
On obtient ainsi une nouvelle classe FirstThread, qui permet de créer des objet tâches (Thread) et qui exécutent le code décrit dans la méthode run.
Comment lancer le
thread, une fois crée?
Attention ! Il ne suffit pas
pour
lancer le thread, d'appeler la méthode run
avec un objet
instance de la classe thread créé.
Exemple
(à ne pas faire)
....
FirstThread t= new
FirstThread ("First")
t.run ()
......
|
Ceci a pour conséquence
d'exécuter la méthode (dans le thread courrant)
comme
n'importe quelle autre méthode java.
Afin de lancer effectivement le Thread, il
faut passer par la méthode public void start()
héritée de la classe java.lang.Thread.
Cette méthode permet de récupérer les ressources
nécessaires à l'exécution du thread,
puis l'active
(comme thread séparé du thread dans lequel il a
été appelé).
La méthode start, appelle la méthode run définie
dans le Thread lancée.
/** Premier test de classe de thread en utilisant la * technique qui consiste à dériver de la classe Thread. */
/** Programme qui lance deux threads. * Chacun d'eux possède une données qui lui est * propre. */
static public void main(String argv[]) { FirstThread thr1 = new FirstThread("Toto"); FirstThread thr2 = new FirstThread("Tata"); } }
|
Remarque :
avec cette technique on comme
- avantages :
- autant d'objets que de threads, chaque thread a sa propre
identité
- possibilité d'avoir plusieurs classes
différentes qui dérivent de
java.lang.Thread, avec une méthode run spécifique
à chacune
- inconvénients
- le lien d'héritage (unique en java), est
déja utilisée pour Thread,. Il ne peut pas être
utilisé pour une autre dérivation (Ex. La classe montre)
Créer un thread en implémentant l'interface
java.lang.Runnable.
La seconde technique consiste donc à
implémenter l'interface java.lang.Runnable.
L'interface Runnable est en fait très simple
dans le sens où elle ne définit qu'une unique
méthode : la méthode run.
Pour créer un thread avec cette
technique, on réutilise aussi la classe java.lang.Thread, mais
pas en dérivation.
Cette classe sera utilisée pour instancier un Thread, en
utilisant le constructeur de Thread admettant l'interface Runnable
comme paramètre .
package java.lang;
public class Thread extends Object { private Thread theThread = null;
// ... du code ...
public Thread(Runnable theThread) { this.theThread = theThread; }
// ... du code ...
public void run() { if (this.theThread != null) { theThread.run(); } }
// ... du code ... }
|
Par ce biais, un thread est
initialisé avec l'objet implémentant l'interface
Runnable, et la fonction start invoquera la méthode run (de
l'objet de type Runnable) passé en paramètre au
constructeur du Thread.
/** Second test de classe de thread en utilisant la * technique qui consiste à implémenter l'interface * java.lang.Runnable */
public class SecondThread implements Runnable { /** Un attribut partagé par tous les threads */ private int counter;
/** Démarrage de cinq threads basés sur un même objet */ public SecondThread (int counter) { this.counter = counter;
// On démarre cinq threads sur le même objet for (int i=0;i<5;i++) { (new Thread(this)).start(); /*C'est là que s'établit le lien entre l'objet Runnable et le thread */ } } /** Chaque thread affiche 500 fois un message. Un * unique compteur est partagé pour tous les threads. * Il y a cinq threads. Le dernier affichage devrait * donc être "Valeur du compteur == 2499". */ public void run() { try { for(int i=0;i<500;i++) { System.out.println( "Valeur du compteur == " + counter++ ); Thread.sleep(30); } } catch (InterruptedException exception) { exception.printStackTrace(); } }
|
/** Le main créer un unique objet sur lequel vont se * baser cinq threads. Il vont donc tous les cinq se * partager le même attribut. */ static public void main(String argv[]) { SecondThread p1 = new SecondThread(0); } }
|
Remarque
Plusieurs Thread peuvent ainsi être
initialisés avec un même objet
Nécessité de synchroniser les accès à
l'objet dans certaines situtions (cf. Synchronisation)
Exemple d'implémentation d'une
classe Clock permettant d'afficher l'heure graphiquement. Pour cela, on
a besoin de dissocier la calcul de l'heure (comptage du temps
écoulétoutes les secondes), de son affichage graphique (2
threads : le thread courant qui gère l'affichage et un thread
que l'on crée pour changer le contenu à afficher à
chaque seconde).
Cette classe
dérive de la classe java.awt.Label. Cet objet label peut
être ajouté à une interface graphique.
En plus , cette classe fournit la méthode run
requise par l'interface Runnable.
Il ne reste plus qu'à lancer
un objet de thread sur l'objet label pour pouvoir changer son contenu
à chaque seconde.
import java.util.*; import java.text.*; import java.awt.*;
public class Clock extends Label implements Runnable { private DateFormat timeFormat = DateFormat.getTimeInstance();
public Clock() { this.setText(timeFormat.format(new Date())); this.setAlignment( Label.CENTER ); (new Thread(this)).start(); }
public void run() { try { while(true) { this.setText(timeFormat.format(new Date())); Thread.sleep(1000); } } catch(Exception exception) { exception.printStackTrace(); } }
}
|
Donc deux techniques principales peuvent
être utilisées pour créer un thread.
Résumons leurs avantages et leurs inconvénients.
|
Avantages |
Inconvénients |
extends java.lang.Thread |
Chaque thread a ses données qui lui sont propres. |
On ne peut plus hériter d'une autre classe. |
implements java.lang.Runnable |
L'héritage reste possible. En effet, on peut
implémenter autant d'interfaces que l'on souhaite. |
Les attributs de votre classe sont partagés pour
tous les threads qui y sont basés. Dans certains cas, il peut
s'avérer que cela soit un atout. |
Remarque
Il est aussi possible de partager
des données communes entre threads dans le cas de
l'héritage de java.lang.Thread.
Il suffit pour cela d'utiliser des variables de classe (static).
Gestion des threads
Cycle de vie d'un thread.
Un thread peut passer par différents
états, tout au long de son cycle de vie.
Le diagramme suivant
illustre ces différents stades ainsi que les différentes
transitions possibles.

Comme on le voit sur le schéma, un
thread passe par les états : initial , transitions entre
éxécutable et non exécutable, et fin (mort).
A sa création, un thread est par
défaut dans son état initial. Il faut invoquer la
méthode start pour lui permettre de passer dans un
état exécutable.
Le thread passe à l'état mort soit à la fin
de l'exécution de la méthode run(), soit à
l'exécution de stop().
Un thread à l'état mort ne peut plus redémarrer.
L'état exécutable du thread lui
permet de recevoir le processus et de s'exécuter un certain
temps (quelques millisecondes) puis de laisser la main
à un autre thread executable.
- Dans les systèmes préemptifs (systèmes
actuels ): Le thraed n'a aucune action à faire pour
laisser la main à un autre thread.
- Dans les système non préemptifs (plus rare
actuellement) : le thread doit appeler la methode yield() pour
céder la main à un autre thread.
Un thread passe d'un état
exécutable à un état non
exécutable sous l'invocation de l'une de ces
méthodes :
- Thread.sleep(long
durée), qui suspend le thread durant quelques instants,
où
- les méthodes wait de la classe java.lang.Object.
(cf. synchronisation)
Le passage à un état
exécutable à nouveau, à partir d'un état
non exécutable se fait soit :
- après l'écoulement du temps indiqué
dans la méthode sleep
- soit après la réception d'une notication d'un
autre thread (notify(), notifyAll() )
Démarrage, suspension, reprise et
arrêt d'un thread.
Pour gérer l'exécution des
threads, on dispose de différentes méthodes dont celles
ci-dessous.
-
public void start() : cette
méthode permet de
démarrer un thread. En effet, si on invoque la méthode run
(au lieu de start),
le code
s'exécute bien, mais aucun nouveau thread n'est lancé
dans le système.
Inversement, la méthode start,
lance un nouveau thread dans le système dont le code à
exécuter démarre par le run.
- public void suspend() : cette
méthode permet d'arrêter temporairement un thread en cours
d'exécution.
- public void resume() : celle-ci
permet de relancer l'exécution d'un thread, au préalable
mis en pause via suspend.
Attention, le thread ne reprend pas
au début du run, mais continue bien là où
il s'était arrêté.
-
public void stop() : cette
dernière méthode permet de stopper, de manière
définitive, l'exécution du thread.
Une autre solution
pour stopper un thread consiste à simplement sortir de la
méthode run.
Gestion de la priorité d'un thread.
On peut, en Java, agir sur la
priorité des threads. Sur une durée
déterminée, un thread ayant une priorité plus
haute recevra plus fréquemment le processeur qu'un autre
thread.
Il exécutera donc, globalement, plus de code.
La priorité d'un thread va pouvoir
varier entre 0 et 10. Mais attention, il n'est en aucun cas garanti que
le système hôte saura gérer autant de niveaux de
priorités.
Des constantes existent et permettent
d'accéder à certains niveaux de priorités :
MIN_PRIORITY (0) - NORM_PRIORITY (5) - MAX_PRIORITY (10).
L'exemple de code qui suit, lance trois
threads supplémentaires. Chacun d'eux se voit affecter une
priorité différente.
Quelque soit le thread
considéré; celui-ci exécute un code très
simpliste : il incrémente indéfiniment un compteur.
Malgré cela, au bout d'un certain temps le thread initial stoppe
les trois autres et l'on regarde le nombre d'incrémentations
réalisé par chacun d'entre eux.
public class ThreadPriority extends Thread { private int counter = 0;
public void run() { while(true) counter++; }
public int getCounter() { return this.counter; }
public static void main(String args[]) throws Exception { ThreadPriority thread1 = new ThreadPriority(); ThreadPriority thread2 = new ThreadPriority(); ThreadPriority thread3 = new ThreadPriority();
thread1.setPriority(Thread.MAX_PRIORITY); thread2.setPriority(Thread.NORM_PRIORITY); thread3.setPriority(Thread.MIN_PRIORITY);
thread1.start(); thread2.start(); thread3.start(); Thread.sleep(5000); thread1.stop(); thread2.stop(); thread3.stop();
System.out.println("Thread 1 : counter == " + thread1.getCounter()); System.out.println("Thread 2 : counter == " + thread2.getCounter()); System.out.println("Thread 3 : counter == " + thread3.getCounter()); } }
|
Le tableau suivant montre les variations du
nombre de constructions, en fonction des priorités
affectées.
Au terme de votre analyse, on peut bien voir que la priorité de
threads est un
mécanisme de contrôle efficace.
Thread 1 |
Thread 2 |
Thread3 |
Thread.NORM_PRIORITY |
Thread.NORM_PRIORITY |
Thread.NORM_PRIORITY |
250
752 409 |
256
697 243 |
251
964 807 |
Thread.MIN_PRIORITY |
Thread.NORM_PRIORITY |
Thread.MAX_PRIORITY |
11 104 663 |
20 673 290 |
1 164 460
398 |
Gestion d'un groupe de threads.
Une autre possibilité
intéressante consiste à regrouper différents
threads. Dans un tel cas, on peut invoquer un ordre sur l'ensemble
des threads du groupe,
ce qui peut dans certains cas
sérieusement simplifier le code.
Pour inscrire un thread dans un groupe, il
faut que le groupe soit initialement créé.
Pour ce faire,
il vous faut instancier un objet de la classe ThreadGroup.
Un fois le
groupe créé, on peut attacher des threads à ce
groupe, via un
constructeur de la classe Thread.
public Thread(ThreadGroup group, String name)
ou
public Thread(Runnable target,String name)
Par la suite, un thread ne pourra en
aucun cas changer de groupe.
Une fois tous les threads attachés
à un groupe, on peut alors invoquer les méthodes
de contrôle d'exécution des threads sur l'objet de groupe.
Les noms des méthodes sont identiques à la classe Thread
: suspend(), resume(), stop(), ...
Synchronisation de threads et accès
aux ressources partagées.
Lorsque qu'on lance une JVM (Machine
Virtuelle Java), on lance un processus. Ce processus possède
plusieurs threads et chacun d'entre eux partage le même espace
mémoire.
En effet, l'espace mémoire est propre au
processus et non à un thread. Cette caractéristique est
à la fois un atout et à la fois une contrainte. En effet,
partager des données pour plusieurs threads est, par
définition, relativement simple. Par contre les choses peuvent
se compliquer sérieusement si la ressource (les
données) partagée est accédée en
modification : il faut synchroniser les accès concurrents.
Pour mieux comprendre les choses, imaginons
que deux threads cherchent à modifier (à
incrémenter) l'état d'un attribut statique, de type
entier, nommé MaClasse.shared. Seul un thread à
la fois peut exécuter du code, mais n'oublions pas que les
systèmes les plus modernes sont préemptifs ! De plus une
instruction telle que MaClasse.shared = MaClasse.shared + 1; se
traduit en plusieurs instructions en langage machine. Imaginez qu'un
premier thread évalue l'expression MaClasse.shared + 1
mais que le système lui hôte le cpu, juste avant
l'affectation, au profit d'un second thread. Ce dernier se doit lui
aussi d'évaluer la même expression. Le système
redonne la main au premier thread qui finalise l'instruction en
effectuant l'affectation, puis le second en fait de même. Au
final de ce scénario, l'entier aura été
incrémenté que d'une seule et unique unité. De
tels scénarios peuvent amener à des comportements
d'applications chaotiques. Il est donc vital d'avoir à notre
disposition des mécanismes de synchronisation.
Notions de verrous.
L'environnement Java offre un premier
mécanisme de synchronisation : les verrous (locks en anglais).
Chaque objet Java possède un verrou et seul un thread à
la fois peut verrouiller un objet.
Si d'autres threads cherchent
à verrouiller le même objet, ils seront endormis
jusqu'à que l'objet soit déverrouillé. Cela permet
de mettre en place ce que l'on appelle plus communément
une
section critique.
Pour verrouiller un objet par un thread, il
faut utiliser le mot clé synchronized. En fait, il y a
deux façons de définir une section critique :
Afin de mieux comprendre les choses, nous
allons étudier un petit exemple : celui-ci va permettre à
plusieurs threads de tracer des pixels en parallèle. Mais
attention, pour que le programme se déroule normalement, il faut
que l'appel à la méthode de tracé soit
synchronisé. Sinon, il y aura des possibilités pour que
certains pixels soient sautés.
L'interface graphique de l'applet
présente deux boutons et une zone de dessin. Les deux boutons
permettent de lancer le tracé dans la zone de dessin. Mais l'un
des boutons lance le tracé sans synchronisation alors que
l'autre le fait en synchronisant les threads. Pour localiser le code
mettant en oeuvre la synchronisation, regardez le code de la
méthode run.
import java.applet.*; import java.awt.*; import java.awt.event.*;
public class Synchronized extends Applet implements ActionListener, Runnable { private Panel pnlButtonBar = new Panel(); private Button btnStartNormal = new Button("Non synchronisé"); private Button btnStartSynchronized = new Button("Synchronisé"); private Canvas cnvGraphic = new Canvas(); private Label lblStatus = new Label("Choisissez un mode d'exécution");
private boolean drawMode = false; private int x, y;
public void init() { pnlButtonBar.setLayout(new FlowLayout()); pnlButtonBar.add(btnStartNormal); pnlButtonBar.add(btnStartSynchronized); btnStartNormal.addActionListener(this); btnStartSynchronized.addActionListener(this);
this.setLayout(new BorderLayout()); this.add(pnlButtonBar, BorderLayout.NORTH);
cnvGraphic.setBackground(Color.white); this.add(cnvGraphic, BorderLayout.CENTER);
this.add(lblStatus, BorderLayout.SOUTH); }
public void plot() { this.cnvGraphic.getGraphics().drawLine(this.x,this.y,this.x,this.y); if (++this.x >= 300) { this.x=0; this.y++; } }
public void run() { while(this.y < 100) { if (drawMode) { synchronized(this) { this.plot(); } } else { this.plot(); } } }
public void actionPerformed(ActionEvent event) { this.drawMode = (event.getSource() == btnStartSynchronized ); this.cnvGraphic.repaint(); this.x = this.y = 0;
(new Thread(this)).start(); (new Thread(this)).start(); (new Thread(this)).start(); } }
|
Attendre l'accès à une
ressource.
Mais poser des verrous ne suffit par toujours.
Dans certains cas, vos threads doivent patienter (pour ne pas consommer
trop de temps CPU) avant qu'une ressource ne soit disponible. Pour
gérer ces cas, l'environnement Java propose aussi un support
pour pouvoir contrôler l'activité de vos threads. Il vous
est possible de stocker (un tableau d'élément de type java.lang.Object)
des threads endormis sur un objet dans le but de la manipuler
(attention, cela n'a rien à voir avec les verrous).
Tout le support nécessaire à
cette gestion est fourni dans la classe Object. Celle-ci
propose notamment quatre méthodes permettant d'endormir un
thread, ainsi que de le réveiller. Pour de plus amples
informations, vous pouvez toujours consulter la
documentation de l'API du JDK.
Pour endormir un thread sur le moniteur, il
vous faut utiliser la méthode wait. Plusieurs prototypes
vous sont fournis afin de pouvoir attendre indéfiniment soit
durant un délai maximal. Il est vrai que la méthode Thread.sleep
permet aussi de faire patienter un thread, mais il sera alors
impossible de faire reprendre l'activité du thread avant la fin
du timeout. Par contre, la méthode Object.wait le
permet.
Pour réveiller des threads endormis,
vous pouvez utiliser les méthodes notify et notifyAll.
Soit vous choisissez de réveiller un unique thread endormi sur
un objet sur lequel il faut se synchroniser, soit vous décidez
de tous les réveiller.
Exemple de producteurs/consommateurs.
Pour mettre en oeuvre un exemple de
synchronisation un peu évolué, nous allons
considérer un cas d'école : un exemple de
producteurs/consommateurs. Dans un tel cas, une ressource
partagée est utilisée par des threads qui produisent et
par des threads qui consomment. Mais attention, il est hors de question
de consommer si rien n'a été produit.
Pour implémenter notre objet
partagé, nous allons coder une pile (Stack en anglais). On
considère une pile bornée., sur laquelle on ne peut
donc pas empiler indéfiniment.
Pour empiler des données
supplémentaires, il faudra attendre que des threads
consommateurs aient dépilé les anciennes données.
Dans ce cas, les choses sont subtiles. L'objet
de synchronisation est clairement la pile. Nous allons donc, au
gré de l'exécution du programme, endormir des threads sur
cet objet. Mais deux types de threads seront à considérer
: ceux qui produisent et ceux qui consomment. Imaginons le
scénario suivant : un thread empile une donnée. Il va
donc utiliser une méthode pour réveiller un
éventuel consommateur. Mais qu'est ce qui garantit que le thread
réveillé ne sera pas un autre producteur ? Si
c'était le cas, nous aboutirions à des cas
d'inter-blocage : le programme n'évoluerait plus, mais ne
terminerait pas. Il nous faudra donc utiliser la méthode notifyAll
pour réveiller les threads. Il faut alors garantir que
tous les threads réveillés qui n'ont pas accès
à la ressource se rendorment rapidement. D'où le code des
méthodes push et pop de la classe suivante.
public class Pile { private int array []; private int size, index;
/** Un constructeur de pile */
public Pile() {
this (5); }
/** Un autre constructeur de pile qui prend la taille de cette dernière */
public Pile(int size) { this.size = size; this.index = 0; this.array = new int[size]; }
/** Renvoie true si la pile est vide */
public boolean isEmpty() { return index == 0; }
/** Renvoie true si la pile est pleine */
public boolean isFull() { return index == size; }
/** Cette méthode synchronisée permet de dépiler une valeur */
public synchronized int pop () { try { while (isEmpty()) { System.out.println("Consommateur Endormi"); wait(); } } catch(Exception e) { e.printStackTrace(); }
int val = array[--index]; notifyAll(); return val; }
/** Cette méthode synchronisée permet d'empiler une valeur */
public synchronized void push(int value) { try { while (isFull()) { System.out.println("Producteur Endormi"); wait(); } } catch(Exception e) { e.printStackTrace(); }
array[index++] = value; notifyAll(); }
} |
Maintenant que notre ressource partagée
est prête, il ne nous reste plus qu'à coder nos
producteurs et nos consommateurs. Dans les deux cas, ces deux types
(classes) de composants partagent tous certaines
caractéristiques : ils travaillent tous sur la même pile
et dans les deux cas, cadencer les choses via un Thread.sleep pourra
permettre une bonne lisibilité des résultats sur la
console. Nous utilisons ici l'héritage pour définir le
tronc commun à tous nos threads. La classe de base se nomme Fonctionneur,
et elle est de plus abstraite : nous ne voulons pas permettre
d'instancier un quelconque objet de ce type là (de plus, nous
n'avons pas d'implémentation de la méthode run).
abstract class Fonctionneur implements Runnable { /** La pile partagée par tous nos threads */
private static Pile p = new Pile(5); /** Un délai d'attente pour chaque thread */
protected long sleepTime;
/** Un constructeur par défaut. Pas de délai d'attente */
public Fonctionneur () { this (0); }
/** Un constructeur qui prend un délai d'attente pour le thread */
public Fonctionneur (long sleepTime) { this.sleepTime = sleepTime; (new Thread(this)).start(); }
/** Permet de pouvoir récupérer la pile */
public Pile getPile() { return p; } }
|
Nous pouvons maintenant
dériver de cette classe nos deux types de threads : les
producteurs (classe Producteur) qui vont produire des valeurs
entières et donc les empiler (tant que cette dernière
n'est pas pleine) et les consommateurs (class Consommateur) qui
vont lire des valeurs entières sur la pile (tant que cette
dernière n'est pas vide).
public class Producteur extends Fonctionneur {
private int value = 0; public Producteur() { super(); }
public Producteur(long sleepTime) { super(sleepTime); }
public void run() { while (true) { try { Thread.sleep ((long)Math.random()*this.sleepTime); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println (this + " empile " + value); getPile().push(value++); } } }
|
public class Consommateur extends Fonctionneur {
public Consommateur() { super(); } public Consommateur(long sleepTime) { super(sleepTime); }
public void run() { while (true) { try { Thread.sleep ((long)Math.random()*this.sleepTime); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println (this + " depile " + getPile().pop()); } } }
|
Maintenant il ne reste plus
qu'à coder une classe de démarrage afin d'initier le
démarrage des threads à synchroniser sur la pile.
public class Start { static public void main (String args[]) { // Démarrage de deux Producteurs new Producteur(1000); new Producteur(1000); // Démarrage de deux Consommateurs
new Consommateur(1000); new Consommateur(1000);
while (true) { // Mise en veille du thread principal
try { Thread.sleep (10000L); } catch (Exception e) { e.printStackTrace(); } } } }
|
Conclusion
Nous venons donc de voir qu'en Java, il est possible de
créer des threads. Deux techniques vous sont proposées.
Chacune d'entre elle ayant des avantages et des inconvénients
(nous avons dans ce chapitre, volontairement un peu utilisé les
deux techniques). De plus nous avons vu qu'il été
possible de contrôler, plus ou moins, finement l'exécution
de vos threads. Il existe même la notion de groupes de threads
permettant une gestion en lots de threads.
Mais comme dans tout langage, s'il n'était pas possible
de synchroniser les accès concurrents aux ressources
partagées, cette solution serait trop souvent inutile. En
conséquence, la notion de verrous (sections critiques) et un
mécanisme de synchronisation (mise en sommeil et réveil
de threads) vous sont proposés.
|