Dans ce cours, vous allez découvrir une fonctionnalité intéressante de Java : la gestion multi-tâche dite multi-processus (Multithreading) ou bien comment faire plusieurs choses en même temps ?… enfin presque !
(Un OS, Operating System peut avoir plusieurs process avec de multiples threads, mais un seul Thread est exécuté à la fois !)
Imaginez que vous deviez lire 4 livres en même temps, car vous devez apprendre un nouveau langage pour un nouveau job. Vous êtes passez à la Fnac pour acheter les meilleurs ouvrages.
Vous commencez par lire quelques lignes du premier, puis vous prenez le second livre, vous lisez quelques lignes, vous passez au troisième, vous lisez quelques lignes, au quatrième, vous lisez un paragraphe et enfin vous revenez au premier, vous lisez quelques lignes et ainsi de suite. On arrête là, sinon ça va devenir fastidieux !
Problématique : Pour passer d’un livre à l’autre en conservant en mémoire l’histoire de chacun des livres, cela demande des compétences particulières.
En développement Java, la mise en place de Threads ou multi-processus permet de réaliser ce type d’activité au sein d’une application en passant d’un ouvrage à l’autre de manière _presque_ » simultanée !
Le processeur ou les processeurs alloue(nt) du temps à chacun des processus (Threads) qui sont des objets java. Il y a un exemple bien connu qui concerne le téléchargement d’une vidéo en streaming (et par conséquent de sa visualisation en temps réel).
La lecture d’une vidéo en streaming nécessite 2 tâches, 2 processus qui s’exécutent en même temps ( ou presque ;) ). Les threads sont généralement utilisés pour des tâches complexes exécutées en en arrière-plan.
Sinon, lorsque vous exécutez une application java, un thread est lancé et est associé à votre méthode main(). Dans cette méthode, vous pouvez lancer d’autres threads.
Et bien dans le package java.lang, donc inutile d’importer quoi que ce soit !
(Source : Diagramme d’états - illustration JMDoudoux)
Rien de mieux que la pratique pour comprendre comment cela fonctionne.
Nous allons utiliser les classes et interface suivantes :
Remarques : Un objet qui représente un Thread doit être un objet de type Thread (obligatoire !) Un objet ne permet de lancer qu’un et un seul Thread.
Pour des raisons de simplicité j’ai mis les 2 classes dans le même fichier sur JDoodle.
/** Exemple de l'utilisation du ThreadSimple Création de 2 instances de la classe ThreadSimple. Le constructeur de ThreadSimple attends une chaine (message) et un entier indiquant le nombre de fois ou le message doit être affiché. */ public class TestThread { public static void main(String args[]) { ThreadSimple thread1 = new ThreadSimple("thread1", 10); ThreadSimple thread2 = new ThreadSimple("thread2", 10); // Exécution des 2 threads avec l'appel de la méthode start() thread1.start(); thread2.start(); } } class ThreadSimple extends Thread { private String message; private int compteur; /** * Constructeur à 2 arguments * @param message * @param combien */ public ThreadSimple( String message , int combien ) { this.message = message; compteur = combien; } /** * Méthode de la classe Thread redéfinie * Cette méthode est appelée par la méthode start() * Observez le résultat dans votre console, faites plusieurs exécutions... */ public void run() { for( int i = 0 ; i < compteur ; i++ ) { System.out.println( message ) ; } } }
Voici le genre de résultat que vous pouvez voir apparaître dans votre console
thread1 thread1 thread1 thread1 thread2 thread2 thread2 thread2 thread2 thread2 thread2 thread2 thread2 thread2 thread1 thread1 thread1 thread1 thread1 thread1
Vous voyez que l’on appelle jamais la méthode run(), mais uniquement la méthode start(). Le fait d’hériter de la clase Thread nous permet d’implémenter d’autres interfaces mais nous ne pouvons pas hériter d’une autre classe ! Il est préférable d’implémenter l’interface comme dans l’exemple suivant.
Voici une autre écriture pour mettre en place en Thread en implémentant l’interface Runnable.
Cette interface nous oblige à implémenter une seule méthode que nous avons déjà utilisée plus haut. Nous utiliserons aussi la méthode yield()
Comme pour l’exemple précédent, tout est dans le même fichier.
/** * Projet : @ThreadTestYield * Classe : @TestThread.java * @author : Pbouget * Date : 2022 * Cette classe permet d'instancier nos tâches et de les lancer. */ public class TestThread { public static void main(String argv[]) { /* Création de 3 instances de la classe ThreadSimpleRunnable : Remarquez la syntaxe différente par rapport au premier exemple ! Notre classe ThreadSimpleRunnable qui implémente l'interface Runnable est instanciée dans le constructeur du Thread. */ Thread threadSimple1 = new Thread(new ThreadSimpleRunnable("threadSimple1: Bingo !",5)); Thread threadSimple2 = new Thread(new ThreadSimpleRunnable("threadSimple2: Pouet pouet",7)); Thread threadSimple3 = new Thread(new ThreadSimpleRunnable("threadSimple3: Dormir !",5)); // Exécution des 3 threads : threadSimple1.start(); threadSimple2.start(); threadSimple3.start(); } } /** * * Projet : @ThreadTestYield * Classe : @ThreadSimpleRunnable.java * @author : Philippe Bouget * Date : 2022 * Cette classe implements l'interface Runnable */ class ThreadSimpleRunnable implements Runnable { private Object message; private int nombreDePassages; /** * Constructeur avec 1 argument * @param message */ public ThreadSimpleRunnable(Object message) { this(message, 1); } /** * Constructeur avec 2 arguments * @param message * @param nombreDePassages */ public ThreadSimpleRunnable(Object message, int nombreDePassages ) { this.message = message; this.nombreDePassages = nombreDePassages; } /** * Méthode run() est redéfinie. * Que fait le Thread ? */ public void run() { for(int i = 0 ; i < nombreDePassages; i++) { /* * La méthode static yield() permet l'exécution en simultanée. * Elle renvoie le Thread actif dans le groupe des Threads prêts. * Yield signifie abandon, se rendre. On le met pour des threads * peu importants. * Un autre Thread prêt peut donc devenir actif. Nous verrons * l'intérêt ultérieurement. */ Thread.yield(); System.out.println(i+" "+message); } } }
0 threadSimple3: Dormir ! 0 threadSimple2: Pouet pouet 0 threadSimple1: Bingo ! 1 threadSimple2: Pouet pouet 1 threadSimple3: Dormir ! 2 threadSimple2: Pouet pouet 1 threadSimple1: Bingo ! 3 threadSimple2: Pouet pouet 4 threadSimple2: Pouet pouet 2 threadSimple3: Dormir ! 5 threadSimple2: Pouet pouet 6 threadSimple2: Pouet pouet 2 threadSimple1: Bingo ! 3 threadSimple3: Dormir ! 3 threadSimple1: Bingo ! 4 threadSimple1: Bingo ! 4 threadSimple3: Dormir !
La méthode publique yield() applique une pause à l’exécution du thread en cours, ceci pour libérer le processeur qui a d’autres chats à fouetter ! Contrairement à la méthode sleep() que nous verrons plus tard, notre méthode yield() est statique : Thread.yield().
La méthode yield() est pratique dans un contexte où plusieurs threads possèdent la même priorité. Cependant, nous n’avons aucune garantie dans l’ordre d’exécution des threads.
Observez la syntaxe particulière :
Thread threadSimple = new Thread(new ThreadSimpleRunnable("Bingo !",5));
Le fait que la classe ThreadSimpleRunnable implémente l’interface Runnable nous oblige à encapsuler l’instance de cette classe (donc l’objet créé avec new()) dans un objet de la classe Thread. L’interface Runnable nous oblige à implémenter la méthode run() pour exprimer ce que doit faire ce processus ! Ensuite, la méthode start() est toujours appelée pour lancer chacun des processus. Cette méthode appelle elle-même la méthode run().
/** * Classe de test */ public class Test3Threads { public static void main(String args[]) { Tache thread1 = new Tache("1"); Tache thread2 = new Tache("2"); thread1.start(); thread2.start(); int i =0; while (i < 20) { System.out.println("Thread principal"); i++; Thread.yield(); } } } /** * Classe qui hérite de Thread */ class Tache extends Thread { public Tache(String s) { super(s); } public void run() { int i =0; while (i < 10) { System.out.println("Thread n° " + this.getName()); i++; Thread.yield(); } } }
Utilisons la méthode sleep()
public class TestThreadSleep { public static void main(String argv[]) { // Création de 2 instances de la classe ThreadSimpleSleep : Thread threadSimple1 = new Thread(new ThreadSimpleSleep("Piff",5)); Thread threadSimple2 = new Thread(new ThreadSimpleSleep("Paff",5)); //Exécution des 2 threads. threadSimple1.start(); System.out.println("threadSimple1 : Etat = "+threadSimple1.getState()); try { // un petit roupillon pour le Thread en cours (0.5 seconde) //Thread.sleep(500); Thread.sleep(1000); // 1 seconde System.out.println("threadSimple1 : Etat = "+threadSimple1.getState()); } catch(InterruptedException e) {} // on lance le second : threadSimple2.start(); System.out.println("threadSimple2 : Etat = "+threadSimple2.getState()); } } /** * * Projet : @TestThreadSleep * Classe : @ThreadSimpleSleep.java * @author : Philippe Bouget * Date : 2022 * Démonstration de l'utilisation de la méthode * sleep() pour endormir un processus */ class ThreadSimpleSleep implements Runnable { private Object message; private int nombreDePassages; public ThreadSimpleSleep(Object message) { this(message, 1); } public ThreadSimpleSleep(Object message, int nombreDePassages ) { this.message = message; this.nombreDePassages = nombreDePassages; } public void run() { for(int i = 0 ; i < nombreDePassages; i++) { try { /* * Appel de la méthode sleep pour endormir le * processus en cours pendant 1000 millisecondes */ Thread.sleep(1000); // dort pendant 1 seconde }catch(InterruptedException e) {} System.out.println(message); } } }
Ici, vous constatez que nous appelons la méthode sleep() en lui passant en paramètres 1000 millisecondes, soit un roupillon d’une seconde pour le Thread en cours.
La syntaxe est la suivante :
Thread.sleep(1000);
On utilise le nom de la classe Thread pour appeler la méthode de classe sleep(), on ne passe pas par la référence de l’objet.
Dans la classe TestThread, regardez le code, on demande au premier Thread de dormir pendant 1 seconde. Vous voyez que j’ai ajouté appelé la méthode getState() pour connaître l’état des différents threads.
Voici le genre de résultat que vous pouvez obtenir dans votre console :
threadSimple1 : Etat = RUNNABLE Piff threadSimple1 : Etat = TIMED_WAITING threadSimple2 : Etat = RUNNABLE Piff Paff Piff Paff Piff Paff Piff Paff Paff
Comment peut-on tuer un Thread ?
Ce n’est pas gentil, mais il faut savoir le faire !
La méthode stop() est obsolète depuis belle lurette. Vous devez définir un attribut booléen à vrai par défaut.
Il vous suffit d’ajouter ce dernier dans votre méthode run() sous la forme d’une boucle while() dans laquelle vous allez mettre vos instructions. Le changement d’état du booléen permettra d’arrêter le processus pour le Thread concerné. Ce principe ne fonctionne apparemment pas avec une implémentation de l’interface Runnable.
Exemple de code :
private boolean run = true; // ... public void run() { while(run) { // vos instructions pour le changement d'état du booléen } }
public class MainPrincipal { public static void main(String[] args) throws InterruptedException { int index = 1; for (int i = 0; i < 2; i++) { System.out.println("MainPrincipal exécuté : " + index++); Thread.sleep(3000); // roupillon de 3 secondes } ThreadSecondaire threadSecondaire = new ThreadSecondaire(); threadSecondaire.start(); for (int i = 0; i < 3; i++) { System.out.println("MainPrincipal exécuté : " + index++); Thread.sleep(3000); } System.out.println("MainPrincipal arrêté !"); } } class ThreadSecondaire extends Thread { @Override public void run() { int index = 1; for (int i = 0; i < 10; i++) { System.out.println(" ThreadSecondaire en cours d'exécution : " + index++); try { Thread.sleep(1000); } catch (InterruptedException e) { // on ne fait rien } } System.out.println(" ThreadSecondaire arrêté !"); } } }
Voici le résultat que vous pouvez avoir dans votre console :
MainPrincipal exécuté : 1 MainPrincipal exécuté : 2 MainPrincipal exécuté : 3 ThreadSecondaire en cours d'exécution : 1 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 2 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 3 MainPrincipal exécuté : 4 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 4 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 5 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 6 MainPrincipal exécuté : 5 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 7 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 8 ThreadSecondaire arrêté ! ThreadSecondaire en cours d'exécution : 9
/** * Projet : @ThreadNonSynchronise * Classe : @Compteur.java * @author : Pbouget */ public class Compteur { int nbBillet=0; long somme=0; public void compte() { long somme=0; for(nbBillet=0; nbBillet<20000000;nbBillet++) somme+=50; System.out.println(Integer.toString(nbBillet) + " billets de 50 Euro font : " + Long.toString(somme) + " €"); } }
/** * Projet : @ThreadNonSynchronise * Classe : @Caissier.java * @author : Pbouget */ public class Caissier extends Thread { Compteur compteur; public Caissier(Compteur compteur) { this.compteur = compteur; } public void run() { // appel de la méthode de la classe Compteur qui // effectue le traitement compteur.compte(); } }
/** * Projet : @ThreadNonSynchronise * Classe : @Banque.java * @author : Pbouget * Exemple de Thread non synchronisé */ public class Banque { public static void main(String[] argv) { Compteur compteurCommun=new Compteur(); // Voici des threads qui comptent avec le même compteur : Caissier jojo= new Caissier(compteurCommun); Caissier riri= new Caissier(compteurCommun); jojo.start(); riri.start(); System.out.println("Mince, Jojo et Riri n'arrivent pas au même résultat !"); } }
Voici le genre de résultat que vous allez obtenir :
20000000 billets de 50 Euro font: 648081950 Euros 20000001 billets de 50 Euro font: 679653550 Euros Jojo et Riri n'arrivent pas au même résultat !
Il suffit d’ajouter la méthode synchronized(){} de la manière suivante :
public void run() { // appel de la méthode de la classe Compteur qui // effectue le traitement ici avec Synchronisation synchronized(compteur) { compteur.compte(); } }
et voici le résultat obtenu, Jojo et Riri savent enfin compter !
20000000 billets de 50 Euro font: 1000000000 Euros 20000000 billets de 50 Euro font: 1000000000 Euros
On aurait pu aussi ajouter tout simplement un modificateur synchronized à la méthode compte() de la classe Compteur de la manière suivante :
Ce mot-clé permet de poser un verrou pour bloquer l’accès à un objet si 2 threads doivent accèder à ce dernier. Sinon, contastez vous-même les résultats incohérents du précédent test.
Exemple d’interaction entre Threads
Trois méthodes de base de la classe Thread permettent de gérer l’interaction entre les processus :
Remarque : Ces méthodes sont toujours appelées dans des méthodes synchronisées par le modificateur synchronized.
/** * Projet : @ThreadWaitNotify * Classe : @ThreadMaitre.java * @author : Pbouget * Exemple de Thread Maitre et Esclave. */ public class ThreadMaitre extends Thread { public void run() { try { for( int i = 0; i < 5; i++ ) { // Endormir le thread pendant 0,1 seconde. delay(10) ; // Demander au threadEsclave de se mettre en attente Direction.threadEsclave.attendre() ; // Endormir le thread pendant 0,1 seconde. delay(10) ; // Demander au threadEsclave de reprendre. Direction.threadEsclave.reprendre() ; System.out.println( "cycle : " + (i++) ) ; } // Arrêter le processus threadEsclave. Direction.threadEsclave.terminer() ; } catch (Exception e) { e.printStackTrace(); } } /** * Une attente de t millisecondes * @param t delai d'attente en millisecondes */ public void delay( long t ) { try { Thread.sleep( t ) ; } catch( Exception e ) { System.out.println( "delay() : " + e ) ; } } }
/** * Projet : @ThreadWaitNotify * Classe : @ThreadEsclave.java * @author : Philippe Bouget */ public class ThreadEsclave extends Thread { // Déclarer et initialiser les attributs : // (boolean actif) qui représente les états "vie/mort" du thread. // (boolean suspendu) qui représente les états "en attente/prêt" du thread. private boolean actif = true ; private boolean suspendu = false ; public void run() { try { // Tant que le thread est à l'état "vie" while( actif ) { // Endormez le thread pendant 0,01 seconde delay( 10 ) ; System.out.println("le thread esclave est actif "); // Tester l'état "en attente/prêt" et conformez le thread à cet état. if( suspendu ) suspendre(); } System.out.println("Le thread esclave se termine" ); } catch (Exception e) { e.printStackTrace(); } } /** * Change l'état "en attente/prêt" à "en attente". */ public void attendre() { suspendu = true ; System.out.println("Le thread esclave reçoit l'ordre d'attendre" ); } /** * Provoque l'attente du thread dans la pile de l'objet courant. * Ici synchronisation sur la méthode. */ synchronized private void suspendre() throws InterruptedException { System.out.println("le thread esclave se met en attente"); wait(); } /** * Active un thread en attente dans la pile de l'objet courant. */ synchronized public void reprendre() { System.out.println("Le thread esclave quitte l'attente" ); suspendu = false ; notify(); } /** * Change l'état "vie/mort" à "mort". */ public void terminer() { System.out.println("Le thread esclave reçoit l'ordre de terminer"); actif = false ; } public void delay( long t ) { try { Thread.sleep( t ) ; } catch( Exception e ) { System.out.println( "delay() : " + e ) ; } } }
/** * Projet : @ThreadWaitNotify * Classe : @Direction.java * @author : Pbouget */ public class Direction { // Déclarer les attributs static suivant // ThreadMaitre ThreadEsclave public static ThreadEsclave threadEsclave ; public static ThreadMaitre threadMaitre ; public static void main(String[] args) { threadMaitre = new ThreadMaitre(); threadEsclave = new ThreadEsclave() ; threadMaitre.start() ; threadEsclave.start(); } }
On peut regrouper les Threads en utilisant la classe ThreadGroup.
Exemple de syntaxe :
ThreadGroup les2Roues = new ThreadGroup(“Les 2 Roues”); Thread monThread = new Thread(les2Roues,”monThread”);
Et bien chaque membre du groupe pourra interroger son groupe pour connaître les autres threads membres de ce même groupe.
Exemple :
public void run { Thread[] threadsCopains = new Thread[5] ; // remplit le tableau threadsCopains avec les threads du même groupe. Enumerate(threadsCopains); }
Voici un exemple complet simulant une course entre un vélo, une moto et une Ferrari.
Question : Quel véhicule va remporter la course ?
Lancez le programme et regardez dans votre console !
Pensez à choisir Interactive ou à saisir s dans la zone Stdin Inputs.
Classe TestThread :
import java.util.Scanner; import java.io.IOException; import java.util.InputMismatchException; import java.io.InputStreamReader; public class TestThread { public static void main(String args[]) { //Création du threadGroup ThreadGroup groupe = new ThreadGroup("Les amis Threads et compagnie"); //création des threads MoyenDeTransport velo= new MoyenDeTransport(groupe, 1, "Le vélo"); MoyenDeTransport moto = new MoyenDeTransport(groupe, 5, "La moto"); MoyenDeTransport ferrari = new MoyenDeTransport(groupe, 45, "La Ferrari"); Controle controle= new Controle(groupe); //Lancement des 4 threads du groupe. controle.start(); //==> th[0] velo.start(); //==> th[1] moto.start(); //==> th[2] ferrari.start(); //==> th[3] } public static void sortie (String message) { System.out.println(message); } }
public class MoyenDeTransport extends Thread { boolean stop = false; int vitesse; // vitesse du véhicule String type; // type de véhicule int distance=50; // distance à parcourir public MoyenDeTransport(ThreadGroup groupe, int vitesse, String type) { super(groupe,"MoyenDeTransport"); this.vitesse = vitesse; this.type=type; } public void run() { for (int i=0; i<distance; i+=vitesse) { TestThread.sortie( type + " a pacouru " + i*100/distance + " % de la distance "); Thread.yield(); if (stop) break; } if (!stop) TestThread.sortie(type + " est arrivé(e)"); else TestThread.sortie(type + " est arreté(e)"); } public void arret() { stop=true; } }
public class Controle extends Thread { /* Ce thread permet d'arreter les autres threads du groupe de threads "groupe" en entrant la lettre "s" dans la console. */ public Controle(ThreadGroup groupe) { super(groupe,"Controle"); } public void run() { boolean lire=true; String reponse=null; Thread th[] = new Thread[4]; //Récupération des threads du groupe de threads "groupe". enumerate(th); Scanner scanner = new Scanner(System.in); while (lire) { try { while (reponse==null) { reponse = scanner.nextLine(); } } catch (InputMismatchException e) { System.out.println("Erreur lecture au clavier"); } if (reponse.equalsIgnoreCase("s")) lire=false; else reponse=null; } scanner.close(); System.out.println("------ arret ------->"); } }
Bien entendu, vous pensez que c’est la Ferrari qui remporte la course, pensez à appuyer sur la touche S pour stopper la programme et regardez votre console, avec les threads, on a toujours des surprise !
La moto a pacouru 0 % de la distance Le vélo a pacouru 0 % de la distance La Ferrari a pacouru 0 % de la distance Le vélo a pacouru 2 % de la distance ... La moto a pacouru 40 % de la distance La Ferrari a pacouru 90 % de la distance Le vélo a pacouru 6 % de la distance ... Le vélo a pacouru 4 % de la distance La moto a pacouru 50 % de la distance La moto a pacouru 60 % de la distance Le vélo a pacouru 12 % de la distance ... Le vélo a pacouru 98 % de la distance Le vélo est arrivé(e) La Ferrari est arrivé(e) La moto a pacouru 90 % de la distance La moto est arrivé(e) s ------ arret ------->
Dans ce programme, on stoppe le thread Contrôle correspondant au groupe ce qui fait que tous les threads qui appartiennent au même groupe sont arrêtés ! Cela évite de gérer des arrêts pour chacun des processus. Ceci dit, ils terminent tous, lièvres et tortue !
Les Démons : daemon n’est pas un objet maléfique !
Un démon est un processus qui s’exécute en arrière plan. Contrairement à un thread, un démon qui attend une requête client ou autre chose ne bloque pas l’application. Un démon reste en mémoire tant qu’il reste un thread vivant !
D’ailleurs, c’est le cas du Garbage Collector qui est un Thread initialisé avec setDeamon(true).
La méthode setDaemon(boolean b) permet de transformer un processus en démon.
UnThread demon = new UnThread() ; demon.setDaemon(true);
Remarque : un thread doit être déclaré comme démon lorsqu’il est à l’état vivant mais non fonctionnel, c’est-à-dire avant d’appeler la méthode start().
1) Si un thread Parent crée et lance un thread Enfant, le thread Parent ne pourra se terminer que lorsque le thread Enfant aura aussi terminé son exécution. 2) Pour éviter l’inconvénient du (1), on peut utiliser un thread démon (un thread qui s’arrête automatiquement lorsque le thread qui l’a créé se termine), il suffit d’insérer un appel à la méthode setDaemon() juste avant le démarrage du thread Enfant. 3) Pour suspendre l’activité d’un Thread, on utilise la méthode sleep() qui peut recevoir comme argument un délai en nanosecondes. 4) Une exception de type InterruotedException n’est levée que si le thread endormi est réveillé par un autre. 5) On peut réveiller un Thread grâce à la méthode interrupt(). 6) Avec la méthode join(), un thread peut bloquer sa propre exécution en attendant la fin d’un autre thread. 7) Pour arrêter un Thread, il faut un attribut boolean à true et mettre un while() ou un doWhile() dans la méthode run(). 8) Ne bloque pas l’utilisateur car les threads sont indépendants et vous pouvez effectuer plusieurs opérations en même temps. 9) Vous pouvez effectuer plusieurs opérations ensemble, ce qui vous fait gagner du temps. 10) Les threads sont indépendants, donc cela n’affecte pas les autres threads si une exception se produit dans un seul thread.
Nous utilisons le multithreading plutôt que le multiprocessing parce que les threads utilisent une zone de mémoire partagée. Ils n’allouent pas de zone de mémoire séparée, donc on économise de la mémoire, et le changement de contexte entre les threads prend moins de temps que le processus.
Le Multithreading en Java est principalement utilisé dans les jeux, l’animation,…
A vous de trouver des applciations pour mettre en place le MultiThreading en Java !