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();
}
}


}

Quelle technique choisir ?  

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.
cycle

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 :

  • soit on synchronise un ensemble d'instructions sur un objet, dans ce cas on utilise l'instruction synchronized.

    synchronized(object) {
              // Instructions de manipulation d'une
             // ressource partagée.
    }

  • soit on synchronise directement l'exécution d'une méthode pour une classe donnée, en utilisant le qualificateur synchronized sur la  méthode considérée.

    public synchronized void meth(int param) {
                 // Le code de la méthode synchronizée.
    }

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.