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 unRunnablegarde votre classe métier libre. - Les lambdas transforment la forme
Runnableen une seule ligne. Sous-classerThreadnécessite une classe nommée pour le même code. - Le
Runnableque vous passez peut également être transmis à unExecutorServiceplus tard. La sous-classeThreadest 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 finishesQuelques erreurs que les débutants font ici :
start()est ce qui crée le thread OS. Appelerrun()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 passtart(), aucun parallélisme n'a eu lieu.start()ne peut être appelé qu'une seule fois. UnThreadest à usage unique. Appelerstart()une deuxième fois lèveIllegalThreadStateException. Pour exécuter la même tâche à nouveau, créez un nouveauThreadou utilisez unExecutorService.join()peut lancerInterruptedException. C'est un appel bloquant. Si quelqu'un appelleinterrupt()sur le thread qui attend dansjoin(), 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 :
| Constructeur | Quand 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é ».BLOCKEDsignifie qu'il attend un verrou intrinsèque ;WAITING/TIMED_WAITINGsignifie qu'il est garé danswait(),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 currentThreadThread.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.
Ce qu'il faut retenir de l'exécution :
- Les transitions d'état correspondent au contrat. Avant
start(), les deux threads étaientNEW. Aprèsstart(), ils étaientRUNNABLE(ouTERMINATEDsi le travail était minuscule et s'était terminé avant l'impression). Aprèsjoin(), les deux étaientTERMINATED. C'est le cycle de vie queThread.Statedé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()étaitfalseaprès carstart()n'avait jamais été appelé. Si vous déboguez "rien ne semble s'exécuter en parallèle", vérifiez si vous avez écritstart()ourun(). - La boucle d'interruption n'a pas utilisé
Thread.sleepcomme 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 ligneThread.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
maina 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.