Méthodes des threads Java
Les principales méthodes de contrôle des threads Java — start, run, sleep, join, interrupt, yield, setDaemon — avec les pièges courants.
Thread possède de nombreuses méthodes, mais seule une poignée est réellement utilisée dans du code réel. Ce chapitre passe en revue cette poignée — ce que chacune fait, ce qu'elle ne fait pas, et les erreurs qui semblent correctes jusqu'à ce qu'elles ne le soient plus. Les chapitres précédents les ont introduites en passant ; ici, elles sont examinées individuellement.
Cette page couvre le cycle de vie et le contrôle des threads : start vs run, sleep, join, le protocole d'annulation coopérative interrupt, ainsi que les petits utilitaires (yield, currentThread, nommage, statut daemon, priorité, holdsLock, onSpinWait). Pour une vue d'ensemble de la façon dont un thread se déplace entre NEW, RUNNABLE, BLOCKED, WAITING et TERMINATED, consultez Cycle de vie des threads Java.
start() vs. run()
Le bug de multithreading le plus courant :
Thread t = new Thread(() -> work(), "worker");
t.run(); // wrong: runs work() on the CURRENT thread
t.start(); // right: spawns a new OS thread, returns immediatelystart() est la seule méthode qui crée un nouveau thread OS. run() est le corps du travail — l'appeler directement est simplement une invocation de méthode normale qui retourne lorsque le travail est terminé. Si vous ne voyez pas start(), aucun parallélisme n'a lieu.
start() est également à usage unique. Après le retour de run(), le thread est TERMINATED et ne peut pas être redémarré. Un deuxième appel à start() lève une IllegalThreadStateException.
Thread.sleep(ms)
L'appel statique qui met en attente le thread courant pendant au moins la durée donnée :
Thread.sleep(1500); // sleep 1.5 seconds
Thread.sleep(0, 250); // 250 nanoseconds; precision varies by OS
Thread.sleep(Duration.ofMillis(1500)); // Java 19+ overloadTrois choses à savoir :
- Il lève
InterruptedException. Le sommeil est interruptible — c'est ainsi qu'un worker est informé d'arrêter de dormir et de s'arrêter. Vous propagez soit l'exception (déclarezthrows), soit vous la capturez et réactivez le drapeau avecThread.currentThread().interrupt(). - Il ne libère pas les verrous. Un thread en sommeil conserve tous les verrous qu'il détenait auparavant. Si vous appelez
Thread.sleepà l'intérieur d'un blocsynchronized, aucun autre thread n'entre dans le bloc pendant que vous dormez. C'est presque toujours un bug ; utilisezwaitouCondition.await(voir Communication inter-thread) quand vous devez libérer le verrou. - Le timing est "au moins", pas "exactement". L'OS peut vous réveiller un peu en retard sous charge ; il ne vous réveille jamais en avance.
t.join() et t.join(ms)
Attendre qu'un autre thread se termine :
t.join(); // block until t terminates
t.join(2000); // block up to 2 seconds, then continue regardless
boolean done = t.join(Duration.ofSeconds(2));// Java 19+, returns whether it finishedjoin est la façon de composer un travail parallèle en plusieurs étapes : lancez quelques threads, laissez-les s'exécuter, faites un join sur tous, lisez leurs résultats. join() retourne quand le run() du thread cible a retourné (normalement ou par exception). Il lève également InterruptedException afin que les appelants puissent être interrompus pendant l'attente.
Un point subtil : join(0) signifie "join sans timeout" (c'est-à-dire attendre indéfiniment), et non "join avec un timeout de zéro". Si vous voulez un vrai appel "abandonner immédiatement", utilisez t.isAlive() à la place.
t.interrupt() et le drapeau
Le protocole d'annulation coopérative, en trois appels :
t.interrupt(); // set t's interrupt flag (and unblock sleep/wait/join/park)
t.isInterrupted(); // ask whether the flag is set (does NOT clear)
Thread.interrupted(); // static; ask current thread, and CLEAR the flagLe drapeau n'est qu'un volatile boolean sur l'objet Thread. interrupt() le positionne. Si t est actuellement dans sleep, wait, join, ou LockSupport.park (ou de nombreux appels bloquants java.nio), cet appel bloquant lève immédiatement InterruptedException. Sinon, le drapeau attend que le worker le remarque par lui-même.
Un worker qui veut être interruptible a deux responsabilités :
- Vérifier
Thread.currentThread().isInterrupted()entre les étapes longues. - Dans chaque
catch (InterruptedException e), soit propager, soit réarmer le drapeau avecThread.currentThread().interrupt()— ne jamais l'avaler silencieusement.
while (!Thread.currentThread().isInterrupted()) {
try {
doOneUnit();
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag for the loop check
}
}Thread.yield() — presque jamais le bon outil
Thread.yield(); // hint: please run someone elseUn conseil non contraignant pour le planificateur. L'OS est libre de l'ignorer. Il n'existe pratiquement aucun code de production qui ait besoin de yield — si vous voulez attendre un événement, utilisez un wait, un Condition, ou un Semaphore. Recourez à yield uniquement pour les micro-benchmarks, les harnais de test de deadlock, ou si vous écrivez la JVM elle-même.
Thread.currentThread()
L'accesseur statique pour le thread sur lequel le code appelant s'exécute. Les deux usages que vous rencontrerez :
String who = Thread.currentThread().getName();
Thread.currentThread().interrupt(); // re-arm the flag after catching InterruptedExceptiongetName() est également la façon standard d'étiqueter les lignes de journal pour distinguer les threads dans la sortie de production.
getName / setName
Les noms sont importants pour le débogage. Le nom par défaut (Thread-3) est inutile dans un thread dump.
Thread t = new Thread(this::flush, "flush-loop"); // name at construction (preferred)
t.setName("flush-loop-2"); // rename later if a role changesVous pouvez renommer à tout moment, mais la valeur au moment du dump ou du log est ce que le lecteur verra. Passez toujours un nom au constructeur.
setDaemon(true)
Thread t = new Thread(this::poll, "metrics-poller");
t.setDaemon(true); // BEFORE start(); else IllegalThreadStateException
t.start();Les threads daemon ne maintiennent pas la JVM en vie — quand le dernier thread non-daemon se termine, la JVM interrompt brutalement les daemons. Utilisez-les pour les tâches d'entretien qui doivent mourir avec le programme (timers, videurs de métriques, boucles de polling). Ne les utilisez pas pour du travail dont vous avez réellement besoin de la complétion.
setPriority(int)
t.setPriority(Thread.MAX_PRIORITY); // 10
t.setPriority(Thread.MIN_PRIORITY); // 1
t.setPriority(Thread.NORM_PRIORITY); // 5 (default)Principalement indicatif. Le prochain chapitre couvre les priorités en détail ; pour l'instant, l'essentiel : ne vous fiez pas à elles pour l'exactitude, c'est l'OS qui décide de leur signification.
Thread.holdsLock(obj)
Un utilitaire de débogage statique :
assert Thread.holdsLock(monitor) : "expected to be inside a synchronized block on monitor";Retourne true si le thread appelant détient le moniteur intrinsèque de obj. Utile pour affirmer "cette méthode ne doit être appelée que depuis l'intérieur d'un bloc synchronized" sans payer le coût d'acquisition du verrou sur le chemin normal.
Thread.onSpinWait() — Java 9+
while (!done) {
Thread.onSpinWait(); // hint to the CPU: I'm spinning, slow down
}Un conseil au niveau CPU qui met en pause les pipelines et réduit la consommation d'énergie pendant une boucle de spin serrée. C'est spécifiquement pour le cas très étroit où vous attendez quelques microsecondes qu'un autre thread bascule un drapeau ; ce n'est pas un appel général "céder le CPU". Pour tout ce qui est plus long, utilisez LockSupport.park ou un Condition.
Un exemple concret : la plupart de ces méthodes en un seul endroit
Le programme ci-dessous utilise start, join avec un timeout, interrupt, sleep, isInterrupted et setName ensemble — les méthodes que vous appelleriez réellement en production.
Ce qu'il faut retenir de l'exécution :
- La ligne
bad.run()a affichéran on: main. Aucun nouveau thread n'a été créé.bad.isAlive()étaitfalseensuite carstart()n'a jamais été appelé. Tout programme multithread a ce bug à un moment donné ; une fois que vous l'avez fait, vous ne le refaites plus jamais. slow.join(300)a retourné après environ 300 ms même sislowaurait dormi pendant 2000.isAlive()était encoretrue.join(ms)est l'attente bornée — utile quand vous voulez donner à un worker une chance gracieuse de terminer avant d'escalader.slow.interrupt()a mis fin à sonThread.sleepimmédiatement en lançantInterruptedExceptionà l'intérieur du worker. C'est le contrat : les appels bloquants interruptibles réagissent àinterrupt()en sortant avec l'exception, c'est ainsi que fonctionne l'annulation coopérative en pratique.- Le worker
bookkeepera capturéInterruptedExceptionet réarmé le drapeau avecThread.currentThread().interrupt(). LeisInterrupted()suivant a retournétrue. Sans ce réarmement, le drapeau est perdu et tout code plus haut dans la pile d'appels pense qu'aucune interruption n'a eu lieu. daemon.setDaemon(true)a été appelé avantstart()— l'appeler après aurait lancéIllegalThreadStateException. Et quandmaina retourné, le daemon a été tué en plein sommeil ; la JVM s'est arrêtée car il ne restait aucun thread non-daemon. C'est le compromis du daemon : ne bloque jamais la sortie de la JVM, n'est jamais garanti de se terminer.
Et ensuite
Le prochain chapitre, Priorité des threads Java, couvre la méthode setPriority sur Thread, ce que les priorités font réellement sur des OS réels, et pourquoi vous devriez les traiter comme un conseil plutôt qu'une garantie.