W3docs

Classe Thread en Java

Créez et contrôlez des threads en Java en étendant Thread ou en passant un Runnable — et les compromis entre les deux.

java.lang.Thread est l'objet que vous tenez en main lorsque vous souhaitez démarrer, nommer, joindre, interrompre ou interroger un thread d'exécution. Le chapitre précédent a introduit les threads au niveau conceptuel ; celui-ci est la visite de l'API. Tout ce qui se trouve dans java.util.concurrent — exécuteurs, futures, threads virtuels — est construit par-dessus Thread, il vaut donc la peine de connaître la classe brute même si vous utiliserez généralement les enveloppes de plus haut niveau en code de production.

Deux façons de créer un thread

Un Thread est un Runnable enveloppé dans un objet de contrôle. Il existe deux façons de lui fournir le Runnable :

// 1. Pass a Runnable to the constructor (the modern, preferred form)
Thread a = new Thread(() -> System.out.println("hello from " + Thread.currentThread().getName()));

// 2. Extend Thread and override run()
class HelloThread extends Thread {
  @Override public void run() {
    System.out.println("hello from " + getName());
  }
}
Thread b = new HelloThread();

Les deux fonctionnent ; les deux exécutent votre code sur un nouveau thread. La première forme est celle qu'utilisent pratiquement tous les codes modernes, pour trois raisons :

  • Une classe ne peut étendre qu'une seule autre classe. Si vous étendez Thread, vous ne pouvez rien étendre d'autre — et la partie de votre code qui est le travail n'a presque jamais une bonne raison d'être un thread au sens POO. Passer un Runnable garde votre classe métier libre.
  • Les lambdas transforment la forme Runnable en une seule ligne. Sous-classer Thread nécessite une classe nommée pour le même code.
  • Le Runnable que vous passez peut également être transmis à un ExecutorService plus tard. La sous-classe Thread est verrouillée pour s'exécuter sur son propre thread dédié.

Étendez Thread uniquement lorsque vous souhaitez vraiment ajouter un état ou des méthodes au thread lui-même (rare). Pour tout le reste, passez un Runnable.

Démarrer et attendre

Les deux méthodes que vous utiliserez sur presque chaque thread :

Thread t = new Thread(() -> doWork(), "worker");
t.start();                                    // schedule it; return immediately
t.join();                                     // block the caller until the thread finishes

Quelques erreurs que les débutants font ici :

  • start() est ce qui crée le thread OS. Appeler run() directement exécute le corps sur le thread actuel, de manière synchrone — aucun nouveau thread n'est démarré. C'est le bogue de multithreading le plus courant chez les débutants. Si vous ne voyez pas start(), aucun parallélisme n'a eu lieu.
  • start() ne peut être appelé qu'une seule fois. Un Thread est à usage unique. Appeler start() une deuxième fois lève IllegalThreadStateException. Pour exécuter la même tâche à nouveau, créez un nouveau Thread ou utilisez un ExecutorService.
  • join() peut lancer InterruptedException. C'est un appel bloquant. Si quelqu'un appelle interrupt() sur le thread qui attend dans join(), l'attente se termine avec l'exception. Vous devez la gérer ou la propager.

join(millis) attend au maximum ce nombre de millisecondes avant de retourner, que le thread ait terminé ou non. Utilisez-le lorsque vous souhaitez donner à un worker une chance bornée de se terminer normalement avant d'escalader.

Les constructeurs qui comptent

Thread a de nombreux constructeurs ; en pratique, quatre comptent :

ConstructeurQuand l'utiliser
new Thread(Runnable)Le cas de base. Worker anonyme.
new Thread(Runnable, String name)Presque toujours préférable — les noms apparaissent dans les journaux, les profileurs, les dumps de threads.
new Thread(ThreadGroup, Runnable, String)Lorsque vous avez besoin d'un groupe explicite (rare ; les groupes sont largement dépréciés).
new Thread(ThreadGroup, Runnable, String, long stackSize)Lorsque la pile par défaut (environ 1 Mo) est inadaptée — par ex. récursion profonde ou pression mémoire.

Le constructeur vide new Thread() existe et exécute un run() vide, qui ne fait rien. Il n'y a aucune raison de l'utiliser.

Nommez toujours vos threads. "worker-1", "http-3", "flush-loop" — quel que soit le rôle. Un dump de threads plein de Thread-7, Thread-12, Thread-19 est un dump de threads illisible.

Propriétés d'une instance Thread

La poignée de champs et de getters que vous utiliserez réellement :

t.setName("scanner-2");                       // any time before or after start()
String name = t.getName();

t.setDaemon(true);                            // BEFORE start(); else IllegalThreadStateException
boolean d = t.isDaemon();

t.setPriority(Thread.NORM_PRIORITY);          // 1..10; mostly advisory, see chapter 6
int p = t.getPriority();

Thread.State s = t.getState();                // NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
boolean alive = t.isAlive();                  // true between start() and run() returning
long id = t.threadId();                       // Java 19+; old name: getId()

Deux d'entre elles comptent le plus :

  • setDaemon(true) détermine si le thread maintient la JVM en vie. Voir le chapitre précédent — les daemons meurent avec le programme ; les non-daemons le maintiennent en cours d'exécution jusqu'à ce qu'ils retournent.
  • getState() est ce que vous regardez dans un dump de threads pour diagnostiquer « pourquoi le thread est-il bloqué ». BLOCKED signifie qu'il attend un verrou intrinsèque ; WAITING/TIMED_WAITING signifie qu'il est garé dans wait(), join(), sleep(), LockSupport.park(), etc.

Méthodes statiques sur Thread

Quelques méthodes statiques que vous appellerez depuis l'intérieur du worker :

Thread.currentThread();                       // the thread that's executing this code
Thread.sleep(2000);                           // pause this thread for ~2000 ms
Thread.yield();                               // hint to the scheduler "go ahead and run someone else"
Thread.interrupted();                         // returns and CLEARS the interrupt flag of currentThread

Thread.sleep est la plus courante ; elle lève InterruptedException, donc les appelants doivent la gérer ou la propager. Thread.yield n'est presque jamais le bon outil — c'est un indice vague que la JVM et l'OS peuvent ignorer. Si vous voulez coordonner, utilisez une vraie primitive de synchronisation.

Thread.interrupted() retourne true si le thread actuel a été interrompu, et efface le drapeau. t.isInterrupted() (méthode d'instance, sur un thread différent) retourne le drapeau sans l'effacer. Les confondre est une source courante d'interruptions bloquées.

Interruption : comment demander à un thread de s'arrêter

Il n'existe pas de t.stop() sûr (la méthode existe, mais elle est dépréciée depuis la version 1.1 car elle laisse des verrous maintenus et l'état corrompu). Le protocole d'arrêt coopératif est :

Thread worker = new Thread(() -> {
  while (!Thread.currentThread().isInterrupted()) {
    doOneUnitOfWork();
  }
}, "worker");
worker.start();
// ... later, from somewhere else:
worker.interrupt();
worker.join();

interrupt() définit le drapeau d'interruption du worker. Le worker est censé vérifier le drapeau aux points sûrs et se terminer. Si le worker est bloqué dans sleep, wait, join, ou de nombreux appels java.nio, l'appel bloquant lève InterruptedException immédiatement pour que le thread puisse réagir.

Si vous interceptez InterruptedException et ne souhaitez pas la propager, la convention est de réarmer le drapeau afin que les appelants plus haut dans la pile voient encore l'interruption :

try { Thread.sleep(1000); }
catch (InterruptedException e) {
  Thread.currentThread().interrupt();         // re-arm the flag
  return;                                     // and give up cooperatively
}

Avaler une interruption sans réarmer le drapeau est un bogue. Le drapeau est la façon dont le reste du programme sait que vous avez été invité à vous arrêter.

Un exemple complet : le cycle de vie entier en un programme

Le programme ci-dessous crée deux workers de différentes manières (Runnable, sous-classe), observe leurs transitions d'état, les joint, et illustre le protocole d'interruption sur un troisième worker.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • Les transitions d'état correspondent au contrat. Avant start(), les deux threads étaient NEW. Après start(), ils étaient RUNNABLE (ou TERMINATED si le travail était minuscule et s'était terminé avant l'impression). Après join(), les deux étaient TERMINATED. C'est le cycle de vie que Thread.State décrit.
  • La ligne "t3 ran on thread: main" est le bogue à retenir à jamais. t3.run() a exécuté le corps — sur le thread appelant, de manière synchrone. Aucun nouveau thread n'a été créé. t3.isAlive() était false après car start() n'avait jamais été appelé. Si vous déboguez "rien ne semble s'exécuter en parallèle", vérifiez si vous avez écrit start() ou run().
  • La boucle d'interruption n'a pas utilisé Thread.sleep comme attente principale — elle a simplement vérifié le drapeau en continu, avec un court sommeil occasionnel pour que l'interruption puisse mettre fin au sommeil rapidement. Le contrat est le même dans les deux cas : isInterrupted() est ce que le worker interroge ; interrupt() est ce que le demandeur appelle.
  • Le réarmement du drapeau à l'intérieur du catch (la ligne Thread.currentThread().interrupt()) a préservé le signal pour tout code plus haut dans la pile des appels. Sans cette ligne, une interruption interceptée et ignorée disparaîtrait — ce qui est l'un des moyens les plus faciles d'écrire un thread qui ne s'arrêtera pas proprement.
  • Le daemon à la fin était sur le point de dormir pendant 60 secondes ; à la place, la JVM s'est terminée dès que main a retourné, le tuant en plein sommeil. Les threads daemons peuvent détenir n'importe quel type de ressource — mais ils peuvent aussi être coupés à tout moment, c'est pourquoi vous ne devriez pas leur confier des travaux nécessitant un commit.

Prochaine étape

Le chapitre suivant, Interface Java Runnable, zoome sur Runnable lui-même — ce qu'il est vraiment, pourquoi Callable et Future ont été ajoutés par-dessus, et comment les lambdas ont changé l'ergonomie du passage de travail à un thread.

Pratique

Pratique
Vous appelez `t.run()` (et non `t.start()`) sur un `Thread` dont le `Runnable` affiche le nom du thread actuel. Qu'est-ce que cela affiche ?
Vous appelez `t.run()` (et non `t.start()`) sur un `Thread` dont le `Runnable` affiche le nom du thread actuel. Qu'est-ce que cela affiche ?
Was this page helpful?