W3docs

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
TERMINATED

Chaque é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.

Avertissement

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 appelle notify/notifyAll.
  • Thread.join() — sans timeout, se met en pause jusqu'à ce que le thread cible se termine.
  • LockSupport.park() — la primitive sur laquelle sont construits ReentrantLock.lock(), await(), BlockingQueue.take(), et l'ensemble de java.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());          // TERMINATED

Un 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.

java— editable, runs on the server

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

  • Chacun des six états était atteignable par le code dans le même programme. NEW et TERMINATED sont les cas limites ; les quatre du milieu (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING) sont ceux que vous verrez dans un vrai thread dump.
  • Le blocked-thread a rapporté BLOCKED parce que le détenteur possédait le moniteur synchronized. Si nous avions utilisé un ReentrantLock à la place, le même chemin de code aurait rapporté WAITING (car Lock.lock() se met en pause via LockSupport). Le nom de l'état vous indique quel type d'attente, pas seulement "ce thread est bloqué."
  • Le waiting-thread serait resté en WAITING indéfiniment si main n'avait pas appelé cond.notify(). L'état WAITING n'a pas de timeout — quelqu'un d'autre doit le réveiller. C'est exactement comme un notify manqué produit un deadlock qu'aucune exception ne signale jamais.
  • Le thread qui brûle du CPU a rapporté RUNNABLE qu'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 était TERMINATED. Vous pouvez toujours interroger son nom, son ID et son objet d'état, mais le thread est terminé — isAlive() vaut false et start() 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 — un ExecutorService ré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 Threadsleep, join, yield, interrupt, holdsLock, et les utilitaires statiques — avec les pièges pour chacune.

Pratique

Pratique
Un thread est dans l'état `BLOCKED`. Qu'attend-il ?
Un thread est dans l'état `BLOCKED`. Qu'attend-il ?
Pratique
Un thread dump montre un thread en `RUNNABLE` avec `socketRead0` en haut de sa pile. Que fait-il réellement ?
Un thread dump montre un thread en `RUNNABLE` avec `socketRead0` en haut de sa pile. Que fait-il réellement ?
Pratique
Quel appel laisse un thread en `TIMED_WAITING` plutôt qu'en `WAITING` ?
Quel appel laisse un thread en `TIMED_WAITING` plutôt qu'en `WAITING` ?
Was this page helpful?