LECON 306


Les threads
ans 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 !

Ne tardons pas...
Sommaire du chapitre :

  • Principes et bases
  • Une classe héritée de Thread
  • Utiliser l'interface Runnable
  • Synchronisez vos threads
  • Contrôlez votre animation
  • Ce qu'il faut retenir

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 :
  • faire une classe héritée de la classe Thread ;
  • créer une implémentation de l'interface Runnable et instancier un objet Thread avec une implémentation de cette interface.

Vous devez avoir les sourcils qui se lèvent, là ... un peu comme ça :
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 :

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 :


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 :
  • NEW : lors de sa création.
  • RUNNABLE : lorsque vous invoquez la méthode start(), celui-ci est prêt à travailler.
  • TERMINATED : lorsque celui-ci a terminé toutes ses tâches, on dit aussi que le thread est mort. Une fois un thread mort, vous ne pouvez plus le relancer avec la méthode start() !
  • TIMED_WAITING : lorsque celui-ci est en pause, quand vous utilisez la méthode sleep() par exemple.
  • WAITING : en attente indéfinie...
  • BLOCKED : lorsque l'ordonnanceur met un thread en sommeil pour en utiliser un autre... Le statut de celui en sommeil est celui-ci.

Un thread est considéré comme terminé lorsque la méthode run() est dépilée 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 :


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...
Notre thread va faire autant d'opérations que nous le souhaitons. Voici un petit diagramme de classe résumant la situation :


Je résume.
  • Notre application peut avoir 1 ou plusieurs objets Thread.
  • Ceux-ci ne peuvent avoir qu'un objet de type Runnable.
  • Dans notre cas, nos objets Thread auront une implémentation de Runnable : RunImpl.
  • Celui-ci à un objet CompteEnBanque.


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 :


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



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 :


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 !

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.

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 :
  • le thread A (moi) appelle la méthode M ;
  • je commence par demander une baguette, la boulangère me la pose sur le comptoir et commence à calculer le montant ;
  • c'est là que le thread B (vous) cherche aussi à utiliser la méthode M, cependant, elle est déjà prise par un thread (moi...) ;
  • vous êtes donc mis en attente ;
  • l'action revient sur moi (thread A) ; au moment de payer, je dois chercher de la monnaie dans ma poche...
  • au bout de quelques instant, je m'endors...
  • l'action revient sur le thread B (vous)... Mais la méthode M n'est toujours pas libérée du thread A... Remise en attente ;
  • on revient sur le thread A qui arrive enfin à payer et à quitter la boulangerie, la méthode M est libérée !
  • le thread B (vous) peut enfin utiliser la méthode M ;
  • et là, les threads C, D, E, F, G, H, I, J entrent dans la boulangerie...
  • etc.


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 !

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

Ce qu'il faut retenir

  • Un nouveau thread permet de créer une nouvelle pile d'exécution.
  • La classe Thread et l'interface Runnable se trouvent dans le package java.lang, donc aucun import spécifique est nécessaire pour leur utilisation.
  • Un thread se lance lorsqu'on invoque la méthode start().
  • Cette dernière invoque automatiquement la méthode run().
  • Les traitements que vous souhaitez mettre dans une autre pile d'exécution sont à mettre dans la méthode run(), qu'il s'agisse d'une classe héritée de Thread ou d'une implémentation de Runnable.
  • Pour protéger l'intégrité de vos données accessibles à plusieurs threads, utilisez le mot clé synchronized dans la déclaration de vos méthodes.
  • Un thread est déclaré mort lorsque celui-ci a dépilé sa méthode run() de sa pile d'exécution !
  • Les threads peuvent avoir plusieurs états : BLOCKED, NEW, TERMINATED, WAITING, RUNNABLE et TIMED_WATING.
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.


0 comments to "LECON 306"

Post a Comment

Powered by Blogger.

About This Blog

Aller au debut de la page