W3docs

Métodos de Thread en Java

Métodos de control de hilos en Java — start, run, sleep, join, interrupt, yield, setDaemon — con los errores más comunes en código real.

Thread tiene muchos métodos, pero solo un puñado son los que utilizarás en código real. Este capítulo repasa ese puñado: qué hace cada uno, qué no hace, y los errores que parecen correctos hasta que no lo son. Los capítulos anteriores los mencionaron brevemente; aquí se estudian de forma individual.

Esta página cubre el ciclo de vida y el control del hilo: start vs run, sleep, join, el protocolo de cancelación cooperativa interrupt, además de los ayudantes menores (yield, currentThread, nombres, estado de daemon, prioridad, holdsLock, onSpinWait). Para el panorama general de cómo un hilo transita por NEW, RUNNABLE, BLOCKED, WAITING y TERMINATED, consulta Java Thread Life Cycle.

start() vs. run()

El bug de multithreading más común:

Thread t = new Thread(() -> work(), "worker");
t.run();                                      // wrong: runs work() on the CURRENT thread
t.start();                                    // right: spawns a new OS thread, returns immediately

start() es el único método que crea un nuevo hilo del sistema operativo. run() es el cuerpo del trabajo — llamarlo directamente es simplemente una invocación de método normal que retorna cuando el trabajo termina. Si no ves start(), no hay paralelismo.

start() también es de uso único. Una vez que run() retorna, el hilo está en TERMINATED y no se puede reiniciar. Una segunda llamada a start() lanza IllegalThreadStateException.

Thread.sleep(ms)

La llamada estática que suspende el hilo actual durante al menos la duración indicada:

Thread.sleep(1500);                          // sleep 1.5 seconds
Thread.sleep(0, 250);                        // 250 nanoseconds; precision varies by OS
Thread.sleep(Duration.ofMillis(1500));       // Java 19+ overload

Tres cosas que debes saber:

  • Lanza InterruptedException. Sleep es interrumpible — así se le indica a un worker que deje de dormir y se detenga. O bien propagas la excepción (declaras throws) o la capturas y reestableces el flag con Thread.currentThread().interrupt().
  • No libera los bloqueos. Un hilo dormido retiene todos los bloqueos que tenía antes. Si haces Thread.sleep dentro de un bloque synchronized, ningún otro hilo puede entrar al bloque mientras duermes. Eso es casi siempre un bug; usa wait o Condition.await (ver Inter-thread Communication) cuando necesites liberar el bloqueo.
  • El tiempo es "al menos", no "exactamente". El sistema operativo puede despertarte un poco tarde bajo carga; nunca te despertará antes.

t.join() y t.join(ms)

Esperar a que otro hilo 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 finished

join es cómo compones trabajo paralelo de varios pasos: creas unos cuantos hilos, los dejas correr, haces join en todos y lees sus resultados. join() retorna cuando el run() del hilo objetivo ha retornado (ya sea normalmente o por excepción). También lanza InterruptedException para que los llamadores puedan ser interrumpidos durante la espera.

Un detalle sutil: join(0) significa "join sin tiempo límite" (es decir, esperar para siempre), no "join con tiempo límite cero". Si quieres una llamada que realmente abandone de inmediato, usa t.isAlive().

t.interrupt() y el flag

El protocolo de cancelación cooperativa, en tres llamadas:

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 flag

El flag es simplemente un volatile boolean en el objeto Thread. interrupt() lo activa. Si t está actualmente en sleep, wait, join o LockSupport.park (o muchas llamadas bloqueantes de java.nio), esa llamada bloqueante lanza InterruptedException inmediatamente. De lo contrario, el flag espera a que el worker lo note por sí solo.

Un worker que quiere ser interrumpible tiene dos responsabilidades:

  1. Verificar Thread.currentThread().isInterrupted() entre pasos de larga duración.
  2. En cada catch (InterruptedException e), propagar o reestablecer el flag con Thread.currentThread().interrupt() — nunca ignorarlo en silencio.
while (!Thread.currentThread().isInterrupted()) {
  try {
    doOneUnit();
    Thread.sleep(100);
  } catch (InterruptedException e) {
    Thread.currentThread().interrupt();        // restore flag for the loop check
  }
}

Thread.yield() — casi nunca es la herramienta correcta

Thread.yield();                              // hint: please run someone else

Una sugerencia no vinculante al planificador. El sistema operativo puede ignorarla libremente. Prácticamente no existe código en producción que necesite yield — si quieres esperar un evento, usa wait, un Condition o un Semaphore. Recurre a yield únicamente para micro-benchmarks, arneses de prueba de deadlock, o cuando estés escribiendo la propia JVM.

Thread.currentThread()

El accessor estático para el hilo en el que se está ejecutando el código que llama. Los dos usos que verás:

String who = Thread.currentThread().getName();
Thread.currentThread().interrupt();          // re-arm the flag after catching InterruptedException

getName() también es la forma estándar de etiquetar líneas de log para poder distinguir hilos en la salida de producción.

getName / setName

Los nombres importan para depurar. El nombre por defecto (Thread-3) es inútil en un volcado de hilos.

Thread t = new Thread(this::flush, "flush-loop");      // name at construction (preferred)
t.setName("flush-loop-2");                              // rename later if a role changes

Puedes renombrar en cualquier momento, pero el valor en el momento del volcado o del log es lo que verá el lector. Siempre pasa un nombre al constructor.

setDaemon(true)

Thread t = new Thread(this::poll, "metrics-poller");
t.setDaemon(true);                           // BEFORE start(); else IllegalThreadStateException
t.start();

Los hilos daemon no mantienen viva la JVM — cuando el último hilo no-daemon termina, la JVM elimina los daemons. Úsalos para tareas de mantenimiento que deban morir con el programa (timers, flusheadores de métricas, bucles de sondeo). No los uses para trabajo cuya finalización realmente necesites.

setPriority(int)

t.setPriority(Thread.MAX_PRIORITY);          // 10
t.setPriority(Thread.MIN_PRIORITY);          // 1
t.setPriority(Thread.NORM_PRIORITY);         // 5 (default)

En su mayoría orientativo. El siguiente capítulo cubre las prioridades en detalle; por ahora, el titular: no confíes en ellas para la corrección, el sistema operativo decide qué significan.

Thread.holdsLock(obj)

Un ayudante estático de depuración:

assert Thread.holdsLock(monitor) : "expected to be inside a synchronized block on monitor";

Retorna true si el hilo que llama posee el monitor intrínseco de obj. Útil para afirmar "este método solo debe llamarse desde dentro de un bloque synchronized" sin pagar el costo de adquisición de bloqueo en el camino feliz.

Thread.onSpinWait() — Java 9+

while (!done) {
  Thread.onSpinWait();                       // hint to the CPU: I'm spinning, slow down
}

Una sugerencia a nivel de CPU que pausa los pipelines y reduce el consumo de energía durante un bucle de espera activa ajustado. Es específicamente para el caso muy acotado en que estás girando unos pocos microsegundos esperando que otro hilo cambie un flag; no es una llamada general de "ceder CPU". Para cualquier cosa más larga, usa LockSupport.park o un Condition.

Un ejemplo práctico: la mayoría en un solo lugar

El programa a continuación usa start, join con tiempo límite, interrupt, sleep, isInterrupted y setName juntos — los métodos que realmente llamarías en producción.

java— editable, runs on the server

Lo que debes aprender de la ejecución:

  • La línea bad.run() imprimió ran on: main. No se creó ningún hilo nuevo. bad.isAlive() era false después porque start() nunca fue llamado. Todo programa de multithreading tiene este bug en algún momento; una vez que lo cometes, no lo vuelves a cometer.
  • slow.join(300) retornó después de unos 300 ms aunque slow habría dormido durante 2000. isAlive() seguía siendo true. join(ms) es la espera acotada — útil cuando quieres darle a un worker una oportunidad elegante de terminar antes de escalar.
  • slow.interrupt() terminó su Thread.sleep de inmediato lanzando InterruptedException dentro del worker. Ese es el contrato: las llamadas bloqueantes interrumpibles reaccionan a interrupt() saliendo con la excepción, que es cómo funciona la cancelación cooperativa en la práctica.
  • El worker bookkeeper capturó InterruptedException y rearmó el flag con Thread.currentThread().interrupt(). El isInterrupted() posterior retornó true. Sin ese rearme, el flag se pierde y cualquier código más arriba en la pila de llamadas cree que nunca ocurrió ninguna interrupción.
  • daemon.setDaemon(true) fue llamado antes de start() — llamarlo después habría lanzado IllegalThreadStateException. Y cuando main retornó, el daemon fue eliminado a mitad del sueño; la JVM terminó porque no quedó ningún hilo no-daemon. Ese es el intercambio del daemon: nunca bloquea la salida de la JVM, nunca se garantiza que termine.

Qué sigue

El siguiente capítulo, Java Thread Priority, cubre el método setPriority en Thread, qué hacen realmente las prioridades en sistemas operativos reales, y por qué debes tratarlas como una sugerencia en lugar de una garantía.

Práctica

Práctica
Capturas `InterruptedException` en un worker pero no quieres salir del bucle. ¿Qué debes hacer con el flag de interrupción?
Capturas `InterruptedException` en un worker pero no quieres salir del bucle. ¿Qué debes hacer con el flag de interrupción?
Was this page helpful?