Cycle de vie d'un thread Java
Les états d'un thread Java — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — et comment ils évoluent.
Un thread Java n'a pas beaucoup d'états — six, qui sont tous des valeurs de l'enum Thread.State. Mais ces six états constituent le vocabulaire des thread dumps, des profileurs, et de chaque investigation "pourquoi mon programme est bloqué" que vous aurez jamais à mener. Savoir ce que signifie chaque état et quelles transitions sont possibles transforme un thread dump d'un mur de stack traces en un diagnostic.
Les six états
// java.lang.Thread.State — the six possible thread states
public enum State {
NEW, // created, never started
RUNNABLE, // started; running or ready to run on a CPU
BLOCKED, // waiting for a monitor lock to enter a synchronized block
WAITING, // parked indefinitely (Object.wait, Thread.join, LockSupport.park)
TIMED_WAITING, // parked with a timeout (sleep, wait(ms), join(ms), park(nanos))
TERMINATED // run() has returned
}Les transitions autorisées entre eux forment un cycle de vie simple :
NEW
| start()
v
RUNNABLE <----------+--------+-------+
| | | | |
| | enters | wakes | timeout
| v sync | from | expires
| BLOCKED | wait/ |
| | | join |
| | acquires | |
| v lock | |
+-> RUNNABLE | |
| | |
| wait/join/park | |
v | |
WAITING -------------+ |
| |
| wait(ms)/join(ms)/sleep |
v |
TIMED_WAITING ----------------+
|
| run() returns
v
TERMINATEDChaque état correspond à quelque chose de visible dans un thread dump. Parcourons-les.
NEW
Un Thread que vous avez construit mais sur lequel vous n'avez jamais appelé start(). Aucune ressource OS n'a été allouée ; rien ne s'exécute. Les seules transitions possibles sont :
start()→RUNNABLE- Le thread est récupéré par le garbage collector sans jamais s'être exécuté
Vous pouvez appeler start() exactement une fois. Un deuxième appel lève une IllegalThreadStateException.
RUNNABLE
"Le thread est vivant et s'exécute soit sur un CPU en ce moment, soit est prêt à s'exécuter." Java fusionne les états "running" et "runnable" du système d'exploitation en un seul état — il est impossible de déterminer à partir de Thread.State seul si le thread consomme actuellement du CPU. Le planificateur OS décide quels threads RUNNABLE obtiennent effectivement un cœur à chaque instant.
Un thread RUNNABLE est également l'état dans lequel se trouve un thread bloqué sur des I/O (InputStream.read, Socket.read, FileChannel.read). Cela surprend beaucoup de gens : le thread est "prêt à s'exécuter" uniquement dans le sens où rien dans la JVM ne le bloque. L'OS sait que le thread attend le disque ; la JVM ne le sait pas, elle rapporte donc RUNNABLE. Si vous voyez dans un thread dump qu'un thread est RUNNABLE et que sa frame du dessus est socketRead0 ou similaire, le thread est bloqué sur un appel système — il ne consomme pas de CPU.
RUNNABLE ne signifie pas "occupé." C'est l'état le plus mal interprété dans un thread dump : un thread arrêté dans un appel I/O bloquant (socketRead0, FileInputStream.read) rapporte RUNNABLE même s'il utilise zéro CPU. Ne concluez pas qu'un thread est actif à partir de son état — lisez sa frame de stack supérieure, ou profitez d'un profileur pour l'échantillonner.
BLOCKED
Le thread se trouve à la porte d'un bloc synchronized en attente du verrou moniteur. Un autre thread l'a ; celui-ci est mis en file d'attente. Dès que le détenteur le libère, l'un des threads en attente gagne le verrou et passe à RUNNABLE.
BLOCKED est spécifique à synchronized — le mécanisme de verrou intrinsèque intégré dans la JVM. Le code qui attend un ReentrantLock n'affiche pas BLOCKED ; il affiche WAITING (car ReentrantLock est implémenté par-dessus LockSupport.park). C'est une distinction mineure mais importante lorsque vous lisez des dumps.
La signature classique dans un thread dump pour BLOCKED :
"worker-3" #19 prio=5 ... waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.acme.Cache.put(Cache.java:42)
- waiting to lock <0x000000076ab8e220> (a java.util.HashMap)
at com.acme.Cache.miss(Cache.java:67)Deux informations : quel moniteur vous attendez (<0x000000076ab8e220>) et quelle méthode se trouve à la porte. Cherchez dans le même dump - locked <0x000000076ab8e220> et vous aurez trouvé le thread qui le détient.
WAITING
Le thread a choisi d'attendre indéfiniment. Trois choses mettent un thread dans cet état :
Object.wait()— libère le moniteur et se met en pause jusqu'à ce que quelqu'un appellenotify/notifyAll.Thread.join()— sans timeout, se met en pause jusqu'à ce que le thread cible se termine.LockSupport.park()— la primitive sur laquelle sont construitsReentrantLock.lock(),await(),BlockingQueue.take(), et l'ensemble dejava.util.concurrent.
Un thread en état WAITING n'utilise pratiquement aucune ressource au-delà de sa pile. Il ne pourra effectuer une transition que si quelqu'un d'autre fait quelque chose — un notify, un thread cible qui se termine, un LockSupport.unpark. Si rien ne le réveille jamais, il reste là indéfiniment. C'est ainsi que les deadlocks silencieux apparaissent dans un dump : deux threads, tous deux en WAITING, tous deux détenant ce que l'autre veut.
TIMED_WAITING
Même idée que WAITING, mais avec une échéance. Le thread se réveillera de lui-même à l'expiration du timeout même si rien d'autre ne se produit. Ce qui produit TIMED_WAITING :
Thread.sleep(ms)Object.wait(ms)Thread.join(ms)LockSupport.parkNanos(...),LockSupport.parkUntil(...)BlockingQueue.poll(timeout, unit),Future.get(timeout, unit), etc.
Si un thread est régulièrement bloqué en TIMED_WAITING pendant la durée que vous avez spécifiée, ce n'est pas un bug. S'il reste là au-delà du timeout, il a été remis en pause — quelqu'un a appelé wait(1000) dans une boucle ou la file d'attente est toujours vide.
TERMINATED
run() est retourné (normalement ou par exception). Le thread est terminé ; il ne peut pas être redémarré. t.isAlive() retourne false. Vous pouvez toujours lire son nom et son ID à des fins de journalisation et de débogage, mais le thread lui-même est terminé.
Lire l'état depuis votre propre code
Thread.State est publiquement interrogeable, mais la valeur est un instantané — elle peut changer entre l'appel et votre utilisation. En production, vous ne branchez presque jamais dessus ; vous l'utilisez pour la journalisation et les diagnostics. La JVM expose également ThreadMXBean pour les thread dumps complets, ce que la plupart des tableaux de bord JMX affichent.
Thread t = new Thread(() -> doWork(), "worker");
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (or TIMED_WAITING/BLOCKED/etc., racy)
t.join();
System.out.println(t.getState()); // TERMINATEDUn exemple concret : observer chaque état
Le programme ci-dessous crée des threads qui se retrouvent chacun dans un état différent, puis affiche leur état.
Ce qu'il faut retenir de l'exécution :
- Chacun des six états était atteignable par le code dans le même programme.
NEWetTERMINATEDsont les cas limites ; les quatre du milieu (RUNNABLE,BLOCKED,WAITING,TIMED_WAITING) sont ceux que vous verrez dans un vrai thread dump. - Le
blocked-threada rapportéBLOCKEDparce que le détenteur possédait le moniteursynchronized. Si nous avions utilisé unReentrantLockà la place, le même chemin de code aurait rapportéWAITING(carLock.lock()se met en pause viaLockSupport). Le nom de l'état vous indique quel type d'attente, pas seulement "ce thread est bloqué." - Le
waiting-threadserait resté enWAITINGindéfiniment simainn'avait pas appelécond.notify(). L'étatWAITINGn'a pas de timeout — quelqu'un d'autre doit le réveiller. C'est exactement comme unnotifymanqué produit un deadlock qu'aucune exception ne signale jamais. - Le thread qui brûle du CPU a rapporté
RUNNABLEqu'il soit réellement en train de s'exécuter sur un cœur ou simplement assis dans la file d'exécution en attente d'un. La JVM ne distingue pas "en cours d'exécution" de "prêt" ; l'OS le fait. Si vous devez savoir quels threads consomment réellement du CPU, profilez avec un profileur par échantillonnage —getState()ne vous le dira pas. - Après le retour de
tRunning.join(), son état étaitTERMINATED. Vous pouvez toujours interroger son nom, son ID et son objet d'état, mais le thread est terminé —isAlive()vautfalseetstart()lèverait une exception. Les threads sont à usage unique : quand l'un se termine, vous en créez un nouveau. (C'est la principale motivation de l'executor framework — unExecutorServiceréutilise le même thread OS pour de nombreuses tâches.)
Et ensuite
Le prochain chapitre, Java Thread Methods, parcourt la surface de méthodes de Thread — sleep, join, yield, interrupt, holdsLock, et les utilitaires statiques — avec les pièges pour chacune.