Programmation multithread en Java

Java fournit une prise en charge intégrée de la programmation multithread. Un thread désigne un flux de contrôle séquentiel unique au sein d'un processus. Un processus peut avoir plusieurs threads s'exécutant simultanément, et chaque thread exécute différentes tâches en parallèle.

Le multithread est une forme particulière de multitâche, mais le multithread utilise une surcharge de ressources plus faible.

Voici un autre terme lié aux threads : processus. Un processus comprend l'espace mémoire alloué par le système d'exploitation et contient un ou plusieurs threads. Un thread ne peut pas exister indépendamment ; il doit faire partie d'un processus. Un processus continue de s'exécuter jusqu'à ce que tous les threads non démons aient terminé leur exécution.

Le multithread permet aux programmeurs d'écrire des programmes très efficaces afin d'utiliser pleinement le CPU.


Le cycle de vie d'un thread

Un thread est un processus d'exécution dynamique, et il connaît également un processus allant de la création à la mort.

Le diagramme suivant montre le cycle de vie complet d'un thread.

  • Nouvel état :

    En utilisantnewle mot-clé etThreadAprès qu'une classe ou sa sous-classe crée un objet thread, l'objet thread est dans l'état nouveau. Il reste dans cet état jusqu'à ce que le programmestart()ce thread.

  • État prêt :

    Après que l'objet thread appelle la méthode start(), le thread entre dans l'état prêt. Un thread à l'état prêt se trouve dans la file d'attente des threads prêts et attend d'être ordonnancé par l'ordonnanceur de threads de la JVM.

  • État d'exécution :

    Si un thread à l'état prêt obtient des ressources CPU, il peut exécuterrun(), à ce moment le thread est à l'état d'exécution. Un thread à l'état d'exécution est le plus complexe ; il peut passer à l'état bloqué, à l'état prêt et à l'état mort.

  • État bloqué :

    Si un thread exécute des méthodes telles que sleep, suspend, etc., après avoir perdu les ressources occupées, le thread passe de l'état d'exécution à l'état bloqué. Lorsque le temps de sleep expire ou que des ressources de périphérique sont obtenues, il peut revenir à l'état prêt. Cela peut être divisé en trois types :

    • Blocage en attente : un thread à l'état d'exécution exécute la méthode wait(), ce qui fait passer le thread à l'état bloqué en attente.

    • Blocage synchrone : le thread ne parvient pas à acquérir le verrou synchronized (parce que le verrou est occupé par un autre thread).

    • Autre blocage : lorsqu'une requête d'E/S est émise en appelant sleep() ou join() du thread, le thread entre dans l'état bloqué. Lorsque le délai de sleep() expire, que join() attend que le thread se termine ou expire, ou lorsque le traitement d'E/S est terminé, le thread revient à l'état prêt.

  • État mort :

    Lorsqu'un thread en cours d'exécution termine sa tâche ou que d'autres conditions d'arrêt se produisent, le thread passe à l'état terminé.


Priorité des threads

Chaque thread Java possède une priorité, qui aide le système d'exploitation à déterminer l'ordre dans lequel les threads sont planifiés.

La priorité d'un thread Java est un entier dont la valeur va de 1 (Thread.MIN_PRIORITY) à 10 (Thread.MAX_PRIORITY).

Par défaut, chaque thread se voit attribuer une priorité NORM_PRIORITY (5).

Les threads de priorité plus élevée sont plus importants pour le programme et doivent recevoir des ressources processeur avant les threads de priorité inférieure. Toutefois, la priorité des threads ne garantit pas l'ordre d'exécution des threads et dépend fortement de la plateforme.


Création d'un thread

Java propose trois façons de créer des threads :

  • En implémentant l'interface Runnable ;
  • En étendant la classe Thread elle-même ;
  • En créant des threads via Callable et Future.

Création d'un thread en implémentant l'interface Runnable

Pour créer un thread, la façon la plus simple est de créer une classe qui implémente l'interface Runnable.

Pour implémenter Runnable, une classe n'a besoin d'implémenter qu'une seule méthode, run(), déclarée comme suit :

public void run()

Vous pouvez redéfinir cette méthode. Il est important de comprendre que run() peut appeler d'autres méthodes, utiliser d'autres classes et déclarer des variables, tout comme le thread principal.

Après avoir créé une classe qui implémente l'interface Runnable, vous pouvez instancier un objet thread au sein de la classe.

Thread définit plusieurs constructeurs. Voici celui que nous utilisons souvent :

Thread(Runnable threadOb,String threadName);

Ici, threadOb est une instance d'une classe qui implémente l'interface Runnable, et threadName spécifie le nom du nouveau thread.

Une fois le nouveau thread créé, il ne s'exécutera que lorsque vous appelez sa méthode start().

void start();

Voici un exemple de création d'un thread et de son démarrage :

Exemple

class RunnableDemo implements Runnable { private Thread t; private String threadName; RunnableDemo( String name) { threadName = name; System.out.println("Création de " + threadName ); } public void run() { System.out.println("Exécution de " + threadName ); try { for(int i = 4; i > 0; i--) { System.out.println("Fil : " + threadName + ", " + i); //Faire dormir le thread pendant un moment Thread.sleep(50); } }catch (InterruptedException e) { System.out.println("Fil " + threadName + " interrompu."); } System.out.println("Fil " + threadName + " se termine."); } public void start () { System.out.println("Starting " + threadName ); if (t == null) { t = new Thread (this, threadName); t.start (); } } } public class TestThread { public static void main(String args[]) { RunnableDemo R1 = new RunnableDemo( "Thread-1"); R1.start(); RunnableDemo R2 = new RunnableDemo( "Thread-2"); R2.start(); } }

Compilez le programme ci-dessus. Le résultat d'exécution est le suivant :

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

Création d'un thread en étendant Thread

La deuxième façon de créer un thread consiste à créer une nouvelle classe qui étend la classe Thread, puis à créer une instance de cette classe.

La classe d'extension doit redéfinir la méthode run(), qui est le point d'entrée du nouveau thread. Elle doit également appeler la méthode start() pour s'exécuter.

Bien que cette méthode soit répertoriée comme une approche d'implémentation du multithreading, il s'agit essentiellement d'une instance qui implémente l'interface Runnable.

Exemple

class ThreadDemo extends Thread { private Thread t; private String threadName; ThreadDemo( String name) { threadName = name; System.out.println("Création de " + threadName ); } public void run() { System.out.println("Exécution de " + threadName ); try { for(int i = 4; i > 0; i--) { System.out.println("Fil : " + threadName + ", " + i); //Faire dormir le thread pendant un moment Thread.sleep(50); } }catch (InterruptedException e) { System.out.println("Fil " + threadName + " interrompu."); } System.out.println("Fil " + threadName + " se termine."); } public void start () { System.out.println("Starting " + threadName ); if (t == null) { t = new Thread (this, threadName); t.start (); } } } public class TestThread { public static void main(String args[]) { ThreadDemo T1 = new ThreadDemo( "Thread-1"); T1.start(); ThreadDemo T2 = new ThreadDemo( "Thread-2"); T2.start(); } }

Compilez le programme ci-dessus. Le résultat d'exécution est le suivant :

Creating Thread-1
Starting Thread-1
Creating Thread-2
Starting Thread-2
Running Thread-1
Thread: Thread-1, 4
Running Thread-2
Thread: Thread-2, 4
Thread: Thread-1, 3
Thread: Thread-2, 3
Thread: Thread-1, 2
Thread: Thread-2, 2
Thread: Thread-1, 1
Thread: Thread-2, 1
Thread Thread-1 exiting.
Thread Thread-2 exiting.

Méthodes de Thread

Le tableau suivant répertorie quelques méthodes importantes de la classe Thread :

Non. Description de la méthode
1 public void start()
Provoque le début de l'exécution de ce thread ;JavaLa machine virtuelle Java appelle la méthode run de ce thread.
2 public void run()
Si ce thread a été construit en utilisant un objet Runnable run distinct, alors la méthode run de cet objet Runnable est appelée ; sinon, cette méthode ne fait rien et retourne.
3 public final void setName(String name)
Change le nom de ce thread pour qu'il soit égal au nom de l'argument.
4 public final void setPriority(int priority)
Change la priorité de ce thread.
5 public final void setDaemon(boolean on)
Marque ce thread comme un thread démon ou un thread utilisateur.
6 public final void join(long millisec)
Attend au plus millis millisecondes que ce thread se termine.
7 public void interrupt()
Interrompt ce thread.
8 public final boolean isAlive()
Teste si ce thread est vivant.

Les méthodes ci-dessus sont appelées par des objets Thread. Les méthodes du tableau suivant sont des méthodes statiques de la classe Thread.

Non. Description de la méthode
1 public static void yield()
Met en pause l'objet thread en cours d'exécution et exécute d'autres threads.
2 public static void sleep(long millisec)
Fait dormir (suspend l'exécution) le thread en cours d'exécution pendant le nombre de millisecondes spécifié, sous réserve de la précision et de l'exactitude de la minuterie et de l'ordonnanceur du système.
3 public static boolean holdsLock(Object x)
Renvoie true si et seulement si le thread actuel détient le verrou de moniteur sur l'objet spécifié.
4 public static Thread currentThread()
Renvoie une référence à l'objet thread en cours d'exécution.
5 public static void dumpStack()
Imprime la trace de pile du thread actuel dans le flux d'erreur standard.

Exemple

Le programme ThreadClassDemo suivant illustre certaines méthodes de la classe Thread :

Code du fichier DisplayMessage.java :

//Nom du fichier : DisplayMessage.java //Création d'un thread en implémentant l'interface Runnable public class DisplayMessage implements Runnable { private String message; public DisplayMessage(String message) { this.message = message; } public void run() { while(true) { System.out.println(message); } } }

Code du fichier GuessANumber.java :

//Nom du fichier : GuessANumber.java //Création d'un thread en étendant la classe Thread public class GuessANumber extends Thread { private int number; public GuessANumber(int number) { this.number = number; } public void run() { int counter = 0; int guess = 0; do { guess = (int) (Math.random() * 100 + 1); System.out.println(this.getName() + " guesses " + guess); counter++; } while(guess != number); System.out.println("** Correct !" + this.getName() + "in" + counter + "guesses.**"); } }

Code du fichier ThreadClassDemo.java :

//Nom de fichier : ThreadClassDemo.java public class ThreadClassDemo { public static void main(String [] args) { Runnable hello = new DisplayMessage("Bonjour"); Thread thread1 = new Thread(hello); thread1.setDaemon(true); thread1.setName("hello"); System.out.println("Démarrage du thread hello..."); thread1.start(); Runnable bye = new Message("Au revoir"); Thread thread2 = new Thread(bye); thread2.setPriority(Thread.MIN_PRIORITY); thread2.setDaemon(true); System.out.println("Démarrage du thread goodbye..."); thread2.start(); System.out.println("Démarrage de thread3..."); Thread thread3 = new GuessANumber(27); thread3.start(); try { thread3.join(); }catch(InterruptedException e) { System.out.println("Thread interrompu."); } System.out.println("Démarrage de thread4..."); Thread thread4 = new GuessANumber(75); thread4.start(); System.out.println("main() se termine..."); } }

Le résultat de l'exécution est le suivant ; le résultat diffère à chaque fois.

Starting hello thread...
Starting goodbye thread...
Hello
Hello
Hello
Hello
Hello
Hello
Goodbye
Goodbye
Goodbye
Goodbye
Goodbye
.......

Création d'un thread via Callable et Future

  • 1. Créez une classe d'implémentation de l'interface Callable et implémentez la méthode call(). La méthode call() servira de corps d'exécution du thread et a une valeur de retour.

  • 2. Créez une instance de la classe d'implémentation Callable, et utilisez la classe FutureTask pour encapsuler l'objet Callable. L'objet FutureTask encapsule la valeur de retour de la méthode call() de cet objet Callable.

  • 3. Utilisez l'objet FutureTask comme cible d'un objet Thread pour créer et démarrer un nouveau thread.

  • 4. Appelez la méthode get() de l'objet FutureTask pour obtenir la valeur de retour après la fin d'exécution du thread enfant.

Exemple

public class CallableThreadTest implements Callable<Integer> { public static void main(String[] args) { CallableThreadTest ctt = new CallableThreadTest(); FutureTask<Integer> ft = new FutureTask<>(ctt); for(int i = 0;i < 100;i++) { System.out.println(Thread.currentThread().getName()+"La valeur de la variable de boucle i"+i); if(i==20) { new Thread(ft,"Threads avec valeurs de retour").start(); } } try { System.out.println("Valeur de retour du thread enfant :"+ft.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } @Override public Integer call() throws Exception { int i = 0; for(;i<100;i++) { System.out.println(Thread.currentThread().getName()+" "+i); } return i; } }


Comparaison des trois méthodes de création de threads

  • 1. Lors de la création d'un multithreading en implémentant les interfaces Runnable ou Callable, la classe de thread n'implémente que l'interface Runnable ou Callable et peut toujours hériter d'autres classes.

  • 2. Lors de la création d'un multithreading en héritant de la classe Thread, le code est simple. Si vous devez accéder au thread courant, vous n'avez pas besoin d'utiliser Thread.currentThread() ; vous pouvez directement utiliser this pour obtenir le thread courant.


Plusieurs concepts clés des threads

Dans la programmation multithread, vous devez comprendre les concepts suivants :

  • Synchronisation des threads
  • Communication entre threads
  • Interblocage des threads
  • Contrôle des threads : suspension, arrêt et reprise

Utilisation du multithreading

L'élément clé pour utiliser efficacement le multithreading est de comprendre que les programmes s'exécutent de manière concurrente plutôt que séquentielle. Par exemple, si un programme comporte deux sous-systèmes qui doivent s'exécuter simultanément, la programmation multithread est nécessaire à ce moment-là.

En utilisant le multithreading, vous pouvez écrire des programmes très efficaces. Cependant, veuillez noter que si vous créez trop de threads, l'efficacité d'exécution du programme diminue réellement au lieu d'augmenter.

Rappelez-vous, le coût de la commutation de contexte est également important. Si vous créez trop de threads, le CPU passera plus de temps à changer de contexte qu'à exécuter le programme !

Autres extensions