Aller au menu - Aller au contenu

[Plan du site] Vous êtes ici --- > Le Site du Zéro > Les tutoriels > Officiels > Programmation > Programmation en Java > Java et la programmation événementielle > Les threads > Lecture du tutoriel

Les threads

Avatar
Auteur : cysboy
Note : 17 / 20 (2 votes)
Visualisations : 8 818

Plus d'informations Plus d'informations
Dans ce chapitre, nous allons voir comment créer une nouvelle pile de fonctions et même plusieurs ; tout ceci avec ce qu'on appelle des threads.

Il y a une classe Thread dans java qui gère cela, mais vous allez voir qu'il y a en fait deux façons de créer un nouveau thread, et donc une nouvelle pile d'invocation de méthodes ! :D

Ne tardons pas...
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Principes et bases

Je vous le répète encore mais, lorsque vous lancez votre programme, il y a un thread lancé ! Imaginez que votre thread corresponde à la pile, et, pour chaque nouveau thread créé, celui-ci donne une nouvelle pile d'exécution.

Pour le moment, nous n'allons pas travailler avec notre IHM, nous allons revenir en mode console. Créez-vous un nouveau projet et une classe contenant votre méthode main. Maintenant, testez ce code :

Code : Java
1
2
3
4
5
6
7
8
public class Test {
 
        public static void main(String[] args) {
                
                System.out.println("Le nom du thread principal est " + Thread.currentThread().getName());
                
        }
}


Vous devriez obtenir ceci :
Code : Console
Le nom du thread principal est main


Oui, vous ne rêvez pas... Il s'agit bien de notre méthode main, c'est le thread principal de notre application !
Voyez les threads comme une machine bien huilée capable d'effectuer les tâches que vous lui spécifierez. Une fois instancié, un thread attend son lancement ; une fois celui-ci fait, il invoque sa méthode run() ; c'est dans cette méthode que le thread connaît les tâches qu'il a à faire !

Nous allons maintenant voir comment créer un nouveau thread.
Comme je vous l'ai dit dans l'introduction, il existe deux manières de faire :

Vous devez avoir les sourcils qui se lèvent, là ... un peu comme ça : :o
Ne vous en faites pas, nous allons y aller crescendo...

Une classe héritée de Thread

Nous allons commencer par le plus simple à comprendre.
Comme je vous le disais, nous allons créer un classe héritée, et tout ce que nous avons à faire, c'est redéfinir la méthode run() de notre objet afin qu'il sache quoi faire... Vu que nous allons en utiliser plusieurs, autant pouvoir les différencier par un nom...
Créez la classe correspondant à ce diagramme :

Image utilisateur


On crée ici un constructeur avec un String en paramètre pour spécifier le nom du thread... Cette classe a une méthode getName() afin de retourner celui-ci. La classe Thread se trouve dans la package java.lang, aucune instruction import n'est nécessaire !


Voilà le code de cette classe :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class TestThread extends Thread {
 
                
        public TestThread(String name){
                super(name);
        }
        
        public void run(){
                
                for(int i = 0; i < 10; i++)
                                System.out.println(this.getName());
                
        }       
}


Et maintenant, testez ce code plusieurs fois :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test {
        
        public static void main(String[] args) {
                
                TestThread t = new TestThread("A");
                TestThread t2 = new TestThread("  B");
                t.start();
                t2.start();
        }
}


Voici quelques screenshots de mes tests consécutifs :

Image utilisateur Image utilisateur Image utilisateur Image utilisateur


Vous pouvez voir que l'ordre d'exécution est totalement aléatoire !
Ceci car java utilise un ordonnanceur.
Vous devez savoir que si vous utilisez plusieurs threads dans une application, ceux-ci ne s'exécutent pas en même temps !
En fait, l'ordonnanceur gère les différents thread de façon aléatoire : il va en utiliser un, pendant un certain laps de temps, puis un autre, puis revenir au premier... Jusqu'à ce que les threads soit terminés ! Et, lorsque l'ordonnanceur passe d'un thread à un autre, le thread interrompu est mis en sommeil pendant que l'autre est en éveil !

Un thread peut avoir plusieurs états :

Un thread est considéré comme terminé lorsque la méthode run() est dépilé de sa pile d'exécution !


En effet, une nouvelle pile d'exécution a, à sa base, la méthode run() de notre thread... Une fois celle-ci dépilée, notre nouvelle pile est détruite !

Notre thread principal crée un second thread, celui-ci se lance et crée une pile avec comme base sa méthode run() ; celle-ci appelle methode, l'empile, fait tous les traitements, et, une fois terminé, dépile cette dernière. La méthode run() prend fin, la pile est détruite !

Nous allons un peu modifier notre classe TestThread afin de voir les états de nos threads que nous pouvons récupérer grâce à la méthode getState().

Voici notre classe TestThread modifiée :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class TestThread extends Thread {
 
        Thread t;
                
        public TestThread(String name){
                super(name);
                System.out.println("statut du thread " + name + " = " +this.getState());
                this.start();
                System.out.println("statut du thread " + name + " = " +this.getState());
        }
        
        public TestThread(String name, Thread t){
                super(name);
                this.t = t;
                System.out.println("statut du thread " + name + " = " +this.getState());
                this.start();
                System.out.println("statut du thread " + name + " = " +this.getState());
        }
        
        
        
        public void run(){
                for(int i = 0; i < 10; i++){
                        System.out.println("statut " + this.getName() + " = " +this.getState());
                        if(t != null)System.out.println("statut de " + t.getName() + " pendant le thread " + this.getName() +" = " +t.getState());
                }
        }
        
        public void setThread(Thread t){
                this.t = t;
        }
        
}


Ainsi que notre main :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class Test {
        
        public static void main(String[] args) {
                
                TestThread t = new TestThread("A");
                TestThread t2 = new TestThread("  B", t);
                                
                try {
                        Thread.sleep(1000);
                } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                }
                System.out.println("statut du thread " + t.getName() + " = " + t.getState());
                System.out.println("statut du thread " + t2.getName() + " = " +t2.getState());
                
        }
}


Et un jeu d'essai représentatif :

Image utilisateur


Alors, dans notre classe TestThread, nous avons ajouté quelques instructions d'affichage afin de voir l'état en cours de nos objets, mais nous avons aussi ajouté un constructeur avec un Thread en paramètre supplémentaire, ceci afin de voir l'état de notre premier thread lors de l'exécution du second !

Dans notre jeu d'essai vous pouvez voir les différents statuts qu'ont pris nos threads... Et vous pouvez voir que le premier est BLOCKED lorsque le second est en cours de traitement, ce qui justifie ce dont je vous parlais :
les threads ne s'exécutent pas en même temps !


Vous pouvez voir aussi que les traitements effectués par nos threads sont en fait codés dans la méthode run(). Reprenez l'image que j'ai utilisée :
"un thread est une machine bien huilée capable d'effectuer les tâches que vous lui spécifierez".
Le fait de faire un objet hérité de Thread permet de créer un nouveau thread très facilement. Cependant, vous pouvez procéder autrement, en redéfinissant uniquement ce que doit faire le nouveau thread, ceci grâce à l'interface Runnable. Et dans ce cas, ma métaphore prend tout son sens :
vous ne redéfinissez que ce que doit faire la machine et non pas la machine tout entière !

Utiliser l'interface Runnable

Le fait de ne redéfinir que ce que doit faire notre nouveau thread a aussi un autre avantage... Le fait d'avoir une classe qui n'hérite d'aucune autre ! Eh oui : dans notre précédent test, notre classe TestThread ne pourra plus jamais hériter d'une classe ! Tandis qu'avec une implémentation de Runnable, rien n'empêche votre classe d'hériter de JFrame, par exemple...

Trêve de bavardages : codons notre implémentation de Runnable ; vous ne devriez avoir aucun problème à faire ceci sachant qu'il n'y a que la méthode run() à redéfinir...

Pour cet exemple, nous allons utiliser un exemple que j'ai trouvé intéressant lorsque j'ai appris à me servir des threads...
Vous allez créer un objet CompteEnBanque avec une somme d'argent par défaut, disons 50, et une méthode pour retirer de l'argent (retraitArgent) et une méthode qui retourne le solde (getSolde).
Mais avant de retirer de l'argent, nous irons vérifier que nous ne sommes pas à découvert... :p
Notre thread va faire autant d'opérations que nous le souhaitons. Voici un petit diagramme de classe résumant la situation :

Image utilisateur


Je résume.


Voici les codes source :

RunImpl.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class RunImpl implements Runnable {
 
        private CompteEnBanque cb;
        
        public RunImpl(CompteEnBanque cb){
                this.cb = cb;
        }
        
        public void run() {
                
                for(int i = 0; i < 25; i++){
                                                
                        if(cb.getSolde() > 0){
                                cb.retraitArgent(2);
                                System.out.println("Retrait effectué");
                                                       
                        }                       
                }               
        }
}


CompteEnBanque.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class CompteEnBanque {
 
        private int solde = 100;
        
        public int getSolde(){
                if(this.solde < 0)
                        System.out.println("Vous êtes à découvert !");
                
                return this.solde;
        }
        
        public void retraitArgent(int retrait){
                solde = solde - retrait; 
                System.out.println("Solde = " + solde);                 
        }
}


Test.java



Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Test {
        
        public static void main(String[] args) {
                
                CompteEnBanque cb = new CompteEnBanque();
                
                Thread t = new Thread(new RunImpl(cb));
                t.start();
        }
}


Ce qui nous donne :

Image utilisateur


Rien d'extraordinaire ici... Une simple boucle aurait fait la même chose...
Ajoutons un nom à notre implémentation et créez un deuxième thread utilisant un deuxième compte.
Pensez à modifier votre implémentation afin que nous puissions voir sur quel thread nous sommes. :p
Bon : je suis sympa, voici les codes :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RunImpl implements Runnable {
 
        private CompteEnBanque cb;
        private String name;
        
        public RunImpl(CompteEnBanque cb, String name){
                this.cb = cb;
                this.name = name;
        }
        
        public void run() {
                
                for(int i = 0; i < 50; i++){
                                                
                        if(cb.getSolde() > 0){
                                cb.retraitArgent(2);
                                System.out.println("Retrait effectué par " + this.name);                       
                        }                       
                }               
        }
 
}


Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Test {
        
        public static void main(String[] args) {
                
                CompteEnBanque cb = new CompteEnBanque();
                CompteEnBanque cb2 = new CompteEnBanque();
                                
                Thread t = new Thread(new RunImpl(cb, "Cysboy"));
                Thread t2 = new Thread(new RunImpl(cb2, "ZérO"));
                t.start();
                t2.start();
        }
}


Pour vérifier que nos threads fonctionnent, voici une partie de mon résultat :

Image utilisateur



Jusqu'ici, rien de perturbant... Nous avons utilisé deux instances distinctes de RunImpl utilisant deux instances distinctes de CompteEnBanque.
Mais d'après vous, que ce passerait-il si nous utilisions le même instance de CompteEnBanque dans deux threads différents ? Essayez plusieurs fois ce code :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Test {
        
        public static void main(String[] args) {
                
                CompteEnBanque cb = new CompteEnBanque();
                                                
                Thread t = new Thread(new RunImpl(cb, "Cysboy"));
                Thread t2 = new Thread(new RunImpl(cb, "ZérO"));
                t.start();
                t2.start();
        }
}


Voici juste deux morceaux de résultats obtenus lors de l'exécution :

Image utilisateur Image utilisateur


Vous pouvez voir des incohérences monumentales !
J'imagine que vous avez été comme moi au départ, vous pensiez que le compte aurait été débité de deux en deux jusqu'à la fin, sans avoir ce genre d'abérrations, vu que nous utilisons le même objet... Eh bien non !
Pourquoi ? Tout simplement parce que l'ordonnanceur de java met les threads en sommeil quand il le veut et, lorsque celui qui était en sommeil se réveille, il reprend le travail où il s'était arrêté !

Voyons comment résoudre le problème. ^^

Synchronisez vos threads

Tout est dans le titre ! :D

En gros, ce qu'il faut faire, c'est prévenir la JVM qu'un thread est en train d'utiliser des données qu'un autre thread est susceptible d'utiliser !

Ainsi, lorsque l'ordonnanceur met un thread en sommeil et que celui-ci traitait des données utilisables par un autre thread, ce thread garde la priorité sur les données, et tant que celui-ci n'a pas terminé son travail, les autres threads n'ont pas la possibilité d'y toucher. :magicien:

Ceci s'appelle synchroniser les threads.

Comment fait-on ça ? Je sens que ça va être encore un truc tordu !

Cette opération est très délicate et demande beaucoup de compétences en programmation...
Voici à quoi ressemble votre méthode retraitArgent synchronisée :

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class CompteEnBanque {
 
        private int solde = 100;
        
        public int getSolde(){
                if(this.solde < 0)
                        System.out.println("Vous êtes à découvert !");
                
                return this.solde;
        }
        
        public synchronized void retraitArgent(int retrait){
                solde = solde - retrait;
                System.out.println("Solde = " + solde);
        }
}


Il vous suffit d'ajouter le mot clé synchronized dans la déclaration de votre méthode !
Grâce à ce mot clé, cette méthode est inaccessible à un thread si celle-ci est déjà utilisée par un autre thread ! Les threads cherchant à utiliser des méthodes déjà prises en charge par un autre thread sont mises dans une "liste d'attente".

Je récapitule encore une fois, voici un contexte ludique.
Je serai représenté par le thread A, vous par le thread B et notre boulangerie favorite par la méthode synchronisée M. Voici ce qu'il se passe :


Je pense qu'avec ceci vous avez dû comprendre...
Dans un contexte informatique, il peut être pratique et sécurisé d'utiliser des threads et des méthodes synchronisées lors d'accès à des services distants tel qu'un serveur d'application, ou encore un SGBD...
Les threads, pour soulager le thread principal et ne pas bloquer l'application pendant une tâche et des méthodes synchronisées, pour la sécurité et l'intégrité des données !

Je vous propose maintenant de retourner à notre animation qui n'attend qu'un petit thread pour pouvoir fonctionner correctement ! ^^

Contrôlez votre animation

À partir de là, il n'y a rien de bien compliqué...
Il nous suffit de créer un nouveau thread lorsqu'on clique sur le bouton Go en lui passant une implémentation de Runnable qui, elle, va appeler la méthode go() (ne pas oublier de remettre le booléen de controle à true).

Pour l'implémentation de l'interface Runnable, une classe interne est toute indiquée !


Voici le code de notre classe Fenetre avec le thread :

Code : Java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class Fenetre extends JFrame{
 
        private Panneau pan = new Panneau();
        private JButton bouton = new JButton("Go");
        private JButton bouton2 = new JButton("Stop");
    private JPanel container = new JPanel();
    private JLabel label = new JLabel("Le JLabel");
    private int compteur = 0;
    private boolean animated = true;
    private boolean backX, backY;
    private int x,y ;
    private Thread t;
    
    public Fenetre(){
           
            this.setTitle("Animation");
            this.setSize(300, 300);
            this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            this.setLocationRelativeTo(null);
 
            container.setBackground(Color.white);
            container.setLayout(new BorderLayout());
            container.add(pan, BorderLayout.CENTER);
            
            //Ce sont maintenant nos classes internes qui écoutent nos boutons 
            bouton.addActionListener(new BoutonListener()); 
            
            bouton2.addActionListener(new Bouton2Listener());
            bouton2.setEnabled(false);
            
            JPanel south = new JPanel();
            south.add(bouton);
            south.add(bouton2);
            container.add(south, BorderLayout.SOUTH);
            
            Font police = new Font("Tahoma", Font.BOLD, 16 );
            label.setFont(police);
            label.setForeground(Color.blue);
            label.setHorizontalAlignment(JLabel.CENTER);
            
            container.add(label, BorderLayout.NORTH);
            this.setContentPane(container);
            this.setVisible(true);
                        
    }
        
        private void go(){
        //Les coordonnées de départ de notre rond
                x = pan.getPosX();
                y = pan.getPosY();
        //Pour cet exemple, j'utilise une boucle while
        //Vous verrez qu'elle marche très bien
        while(this.animated){
                
                if(x < 1)backX = false;
            if(x > pan.getWidth()-50)backX = true;               
            if(y < 1)backY = false;
            if(y > pan.getHeight()-50)backY = true;
            
            
                if(!backX)pan.setPosX(++x);
            else pan.setPosX(--x);
            if(!backY) pan.setPosY(++y);
            else pan.setPosY(--y);
            pan.repaint();
 
            try {
                    Thread.sleep(3);
            } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
            }
        }       
        }
 
 
        /**
         * classe qui écoute notre bouton
         */
        public class BoutonListener implements ActionListener{
 
                /**
                 * Redéfinitions de la méthode actionPerformed
                 */
                public void actionPerformed(ActionEvent arg0) {
                        animated = true;
                        t = new Thread(new PlayAnimation());
                        t.start();
                        bouton.setEnabled(false);
                        bouton2.setEnabled(true);
                        
                }
                
        }
        
        /**
         * classe qui écoute notre bouton2
         */
        class Bouton2Listener  implements ActionListener{
 
                /**
                 * Redéfinitions de la méthode actionPerformed
                 */
                public void actionPerformed(ActionEvent e) {
                        animated = false;       
                        bouton.setEnabled(true);
                        bouton2.setEnabled(false);
                }
                
        }       
        
        class PlayAnimation implements Runnable{
 
                @Override
                public void run() {
                        go();                   
                }               
        }       
}


Vous pouvez tester et tester encore, ce code fonctionne très bien ! Vous avez enfin le contrôle sur votre animation ! :magicien:

Ceci fait, nous pouvons allez faire un tour sur le topo, et je crois qu'un QCM vous attend...

Ce qu'il faut retenir


Q.C.M.

Dans quel package se trouvent les ressources nécessaires à l'utilisation des threads ?
Que va afficher ce code ?

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Test extends Thread{
  
   public void run(){
      for(int i = 0; i < 20; i++)
         System.out.println("coucou toi...");  
   }
 
   public static void main(String[] args){
 
        Test t = new Test();
 
   }
 
}

Que va afficher ce code-là ?

Code : Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class Main{
 
  public static void main(String[] args){
      Thread t = new Thread(new Test());
      t.start();
  }
 
  public class Test implements Runnable{
     public void start(){
 
        for(int i = 0; i < 10; i++){
            System.out.println("Salut !");
         }        
    }
  }
}
Quand un thread est-il considéré comme mort ?
Comment protéger les données dans des méthodes susceptibles d'être utilisées par plusieurs threads ?

Statistiques de réponses au QCM


Voilà encore un gros chapitre, et très important qui plus est !
Prenez le temps de bien assimiler les choses et de faire des tests, plein de tests : c'est la meilleure façon de bien comprendre les choses...

Pour ceux qui se sentent d'attaque, en avant pour : les listes.
Chapitre précédent Sommaire Chapitre suivant
Retour en haut Retour en haut


Créé : le 21/06/2006 à 15:02:22
Modifié : le 22/08/2008 à 15:54:13
Avancement : 0%
Licence : Copie non autorisée

Changer de design | En savoir plus | Plan du site | Politique d'accessibilité | Règles | RSS tutoriels | RSS news
Édité par Simple IT SARL : Nous contacter | Notre blog | Revue de presse | Publicité

Y'a plus rien à lire, faut remonter maintenant !

Hébergement web - Correction de tutoriels - Créer un site
Vous souhaitez apparaître ici ? Contactez-nous.

Nombre de connectés 425 Zéros connectés | Requêtes SQL 8 requêtes | Temps de génération de la page : Total (SQL) 0.0394s (0.0223s)