W3docs

Ciclo de vida de los hilos en Java

Los estados de un hilo Java: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED, y cómo cambian entre sí.

Un hilo de Java no tiene muchos estados — seis en total, todos ellos valores del enum Thread.State. Pero esos seis son el vocabulario de los volcados de hilos, los perfiladores y cada investigación de "por qué está colgado mi programa" que hagas alguna vez. Saber qué significa cada estado y qué transiciones son posibles es lo que convierte un volcado de hilos de una pared de trazas de pila en un diagnóstico.

Los seis estados

// 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
}

Las transiciones permitidas entre ellos forman un ciclo de vida 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

Cada estado corresponde a algo visible en un volcado de hilos. Vamos a recorrerlos.

NEW

Un Thread que has construido pero al que nunca has llamado start(). No se han asignado recursos del sistema operativo; nada está en ejecución. Las únicas transiciones de salida son:

  • start()RUNNABLE
  • El hilo es recolectado por el recolector de basura sin haber ejecutado nunca

Puedes llamar a start() exactamente una vez. Una segunda llamada lanza IllegalThreadStateException.

RUNNABLE

"El hilo está vivo y actualmente está ejecutándose en una CPU o está listo para ejecutarse." Java combina los estados "running" y "runnable" del sistema operativo en un único estado — no hay forma de saber solo por Thread.State si el hilo está consumiendo CPU en ese momento. El planificador del sistema operativo decide qué hilos RUNNABLE obtienen realmente un núcleo en cada instante.

Un hilo RUNNABLE también es el estado en que se encuentra un hilo cuando está bloqueado en E/S (InputStream.read, Socket.read, FileChannel.read). Esto sorprende a la gente: el hilo está "listo para ejecutarse" solo en el sentido de que nada en la JVM lo está bloqueando. El sistema operativo sabe que el hilo está esperando al disco; la JVM no, por lo que informa RUNNABLE. Si ves en un volcado de hilos que un hilo es RUNNABLE y su frame superior es socketRead0 o similar, el hilo está bloqueado en una llamada al sistema — no consumiendo CPU.

Advertencia

RUNNABLE no significa "ocupado." Es el estado más malinterpretado en un volcado de hilos: un hilo bloqueado dentro de una llamada de E/S bloqueante (socketRead0, FileInputStream.read) informa RUNNABLE aunque esté usando cero CPU. No concluyas que un hilo está caliente por su estado — lee su frame de pila superior, o muestrea con un perfilador.

BLOCKED

El hilo está esperando en la puerta de un bloque synchronized esperando el bloqueo del monitor. Otro hilo lo tiene; este está en cola. En cuanto el poseedor lo libere, uno de los que esperan gana el bloqueo y vuelve a RUNNABLE.

BLOCKED es específico de synchronized — el mecanismo de bloqueo intrínseco que la JVM incorpora. El código que espera en un ReentrantLock no muestra BLOCKED; muestra WAITING (porque ReentrantLock está implementado sobre LockSupport.park). Es una distinción pequeña pero importante cuando estás leyendo volcados.

La firma clásica de volcado de hilos para 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)

Dos piezas de información: qué monitor estás esperando (<0x000000076ab8e220>) y qué método está en la puerta. Busca en el mismo volcado - locked <0x000000076ab8e220> y habrás encontrado el hilo que lo tiene.

WAITING

El hilo eligió esperar indefinidamente. Tres cosas ponen un hilo aquí:

  • Object.wait() — libera el monitor y se detiene hasta que alguien llame a notify/notifyAll.
  • Thread.join() — sin tiempo de espera, se detiene hasta que el hilo objetivo termina.
  • LockSupport.park() — la primitiva sobre la que están construidos ReentrantLock.lock(), await(), BlockingQueue.take() y todo java.util.concurrent.

Un hilo WAITING consume prácticamente ningún recurso más allá de su pila. No transitará hasta que alguien más haga algo — una notificación, que el objetivo de join termine, un LockSupport.unpark. Si nada lo despierta nunca, permanece allí para siempre. Así es como lucen los interbloqueos silenciosos en un volcado: dos hilos, ambos WAITING, ambos sosteniendo lo que el otro necesita.

TIMED_WAITING

La misma idea que WAITING, pero con un plazo límite. El hilo se despertará a sí mismo cuando expire el tiempo de espera aunque no suceda nada más. Cosas que producen 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 hilo está confiablemente atascado en TIMED_WAITING durante la duración que especificaste, eso no es un error. Si permanece allí más allá del tiempo de espera, ha sido re-detenido — alguien llamó a wait(1000) en un bucle o la cola sigue vacía.

TERMINATED

run() retornó (normalmente o por excepción). El hilo terminó; no puede ser reiniciado. t.isAlive() devuelve false. Aún puedes leer su nombre e ID para propósitos de registro/depuración, pero el hilo en sí ha terminado.

Leer el estado desde tu propio código

Thread.State es consultable públicamente, pero el valor es una instantánea — puede cambiar entre la llamada y tu uso de ella. En producción, casi nunca ramificas sobre él; lo usas para registro y diagnóstico. La JVM también expone ThreadMXBean para volcados completos de hilos, que es lo que muestran la mayoría de los paneles JMX.

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 ejemplo práctico: observar cada estado

El programa a continuación crea hilos que quedan atascados en diferentes estados y luego imprime en qué estado se encuentran.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • Cada uno de los seis estados era alcanzable mediante código en el mismo programa. NEW y TERMINATED son los casos límite; los cuatro del medio (RUNNABLE, BLOCKED, WAITING, TIMED_WAITING) son los que verás en un volcado de hilos real.
  • El blocked-thread informó BLOCKED porque el poseedor tenía el monitor synchronized. Si hubiéramos usado un ReentrantLock en su lugar, la misma ruta de código habría informado WAITING (dado que Lock.lock() se detiene mediante LockSupport). El nombre del estado te dice qué tipo de espera es, no solo "este hilo está atascado."
  • El waiting-thread habría permanecido en WAITING para siempre si main no hubiera llamado a cond.notify(). El estado WAITING no tiene tiempo de espera — alguien más tiene que despertarlo. Así es exactamente como una notificación perdida produce un interbloqueo que ninguna excepción reporta nunca.
  • El hilo que consume CPU informó RUNNABLE tanto si estaba ejecutándose realmente en un núcleo como si simplemente estaba en la cola de ejecución esperando uno. La JVM no distingue "ejecutando" de "listo"; el sistema operativo sí. Si necesitas saber qué hilos están realmente consumiendo CPU, perfila con un perfilador de muestreo — getState() no te lo dirá.
  • Después de que tRunning.join() retornó, su estado era TERMINATED. Aún puedes consultar su nombre, ID y objeto de estado, pero el hilo se ha ido — isAlive() es false y start() lanzaría una excepción. Los hilos son de un solo uso: cuando uno termina, creas uno nuevo. (Esta es la motivación principal del marco de ejecutores — un ExecutorService reutiliza el mismo hilo del sistema operativo para muchas tareas.)

Qué sigue

El siguiente capítulo, Métodos de hilos en Java, recorre la superficie de métodos de Threadsleep, join, yield, interrupt, holdsLock y las utilidades estáticas — con los aspectos problemáticos de cada uno.

Práctica

Práctica
Un hilo está en estado `BLOCKED`. ¿Qué está esperando?
Un hilo está en estado `BLOCKED`. ¿Qué está esperando?
Práctica
Un volcado de hilos muestra un hilo como `RUNNABLE` con `socketRead0` en la parte superior de su pila. ¿Qué está haciendo realmente?
Un volcado de hilos muestra un hilo como `RUNNABLE` con `socketRead0` en la parte superior de su pila. ¿Qué está haciendo realmente?
Práctica
¿Qué llamada deja un hilo en `TIMED_WAITING` en lugar de `WAITING`?
¿Qué llamada deja un hilo en `TIMED_WAITING` en lugar de `WAITING`?
Was this page helpful?