Clase Thread en Java
Crea y controla hilos en Java extendiendo Thread o pasando un Runnable, con las ventajas y desventajas de cada enfoque.
java.lang.Thread es el objeto que usas cuando quieres iniciar, nombrar, unir, interrumpir o consultar un hilo de ejecución. El capítulo anterior introdujo los hilos a nivel conceptual; este es el recorrido por la API. Todo lo que hay en java.util.concurrent — ejecutores, futuros, hilos virtuales — está construido sobre Thread, por lo que vale la pena conocer la clase directamente, aunque en el código de producción generalmente uses los contenedores de mayor nivel.
Dos formas de crear un hilo
Un Thread es un Runnable envuelto en un objeto de control. Hay dos formas de proporcionarle el Runnable:
// 1. Pass a Runnable to the constructor (the modern, preferred form)
Thread a = new Thread(() -> System.out.println("hello from " + Thread.currentThread().getName()));
// 2. Extend Thread and override run()
class HelloThread extends Thread {
@Override public void run() {
System.out.println("hello from " + getName());
}
}
Thread b = new HelloThread();Ambas funcionan; ambas ejecutan tu código en un nuevo hilo. La primera forma es la que usa prácticamente todo el código moderno, por tres razones:
- Una clase solo puede extender una clase más. Si extiendes
Thread, no puedes extender nada más — y la parte de tu código que es el trabajo casi nunca tiene una buena razón para ser un hilo en el sentido de la OO. Pasar unRunnablemantiene tu clase de negocio libre. - Las lambdas convierten la forma
Runnableen una sola línea. Crear una subclase deThreadrequiere una clase con nombre para el mismo código. - El
Runnableque pasas también puede entregarse a unExecutorServicemás adelante. La subclase deThreadestá ligada a ejecutarse en su propio hilo dedicado.
Extiende Thread solo cuando genuinamente quieras añadir estado o métodos al propio hilo (algo poco frecuente). Para todo lo demás, pasa un Runnable.
Iniciar y esperar
Los dos métodos que usarás en casi todos los hilos:
Thread t = new Thread(() -> doWork(), "worker");
t.start(); // schedule it; return immediately
t.join(); // block the caller until the thread finishesAlgunos errores comunes que cometen los principiantes:
start()es lo que crea el hilo del sistema operativo. Llamar arun()directamente ejecuta el cuerpo en el hilo actual, de forma síncrona — no se crea ningún hilo nuevo. Este es el error de multithreading más común entre los principiantes. Si no vesstart(), no ocurrió ningún paralelismo.start()solo se puede llamar una vez. UnThreades de un solo uso. Llamar astart()una segunda vez lanzaIllegalThreadStateException. Para ejecutar la misma tarea de nuevo, crea un nuevoThreado usa unExecutorService.join()puede lanzarInterruptedException. Es una llamada bloqueante. Si alguien llama ainterrupt()en el hilo que está esperando enjoin(), la espera termina con la excepción. Debes manejarla o propagarla.
join(millis) espera como máximo esos milisegundos antes de retornar, tanto si el hilo terminó como si no. Úsalo cuando quieras darle a un trabajador una oportunidad acotada de terminar limpiamente antes de escalar.
Los constructores que importan
Thread tiene muchos constructores; en la práctica, cuatro son relevantes:
| Constructor | Cuándo usarlo |
|---|---|
new Thread(Runnable) | El caso base. Trabajador anónimo. |
new Thread(Runnable, String name) | Casi siempre preferible — los nombres aparecen en logs, perfiladores y volcados de hilos. |
new Thread(ThreadGroup, Runnable, String) | Cuando necesitas un grupo explícito (poco frecuente; los grupos están en gran parte obsoletos). |
new Thread(ThreadGroup, Runnable, String, long stackSize) | Cuando la pila predeterminada (alrededor de 1 MB) no es adecuada — por ejemplo, recursión profunda o presión de memoria. |
El constructor vacío new Thread() existe y ejecuta un run() vacío, que no hace nada. No hay razón para usarlo.
Nombra siempre tus hilos. "worker-1", "http-3", "flush-loop" — cualquiera que sea el rol. Un volcado de hilos lleno de Thread-7, Thread-12, Thread-19 es un volcado que no puedes leer.
Propiedades de una instancia de Thread
El puñado de campos y getters que realmente usarás:
t.setName("scanner-2"); // any time before or after start()
String name = t.getName();
t.setDaemon(true); // BEFORE start(); else IllegalThreadStateException
boolean d = t.isDaemon();
t.setPriority(Thread.NORM_PRIORITY); // 1..10; mostly advisory, see chapter 6
int p = t.getPriority();
Thread.State s = t.getState(); // NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
boolean alive = t.isAlive(); // true between start() and run() returning
long id = t.threadId(); // Java 19+; old name: getId()Dos de estos importan más:
setDaemon(true)decide si el hilo mantiene viva la JVM. Consulta el capítulo anterior — los daemons mueren con el programa; los no-daemons lo mantienen en ejecución hasta que retornan.getState()es lo que miras en un volcado de hilos para diagnosticar "por qué está bloqueado el hilo".BLOCKEDsignifica que está esperando un bloqueo intrínseco;WAITING/TIMED_WAITINGsignifica que está estacionado enwait(),join(),sleep(),LockSupport.park(), etc.
Métodos estáticos de Thread
Algunos métodos estáticos que llamarás desde dentro del trabajador:
Thread.currentThread(); // the thread that's executing this code
Thread.sleep(2000); // pause this thread for ~2000 ms
Thread.yield(); // hint to the scheduler "go ahead and run someone else"
Thread.interrupted(); // returns and CLEARS the interrupt flag of currentThreadThread.sleep es el más común; lanza InterruptedException, por lo que los llamadores deben manejarla o propagarla. Thread.yield casi nunca es la herramienta correcta — es una sugerencia vaga que la JVM y el SO pueden ignorar. Si quieres coordinar, usa una primitiva de sincronización real.
Thread.interrupted() devuelve true si el hilo actual ha sido interrumpido, y borra el indicador. t.isInterrupted() (método de instancia, en un hilo diferente) devuelve el indicador sin borrarlo. Confundirlos es una fuente común de interrupciones atascadas.
Interrupción: cómo pedirle a un hilo que se detenga
No existe un t.stop() seguro (el método existe, pero está obsoleto desde la versión 1.1 porque deja bloqueos activos y el estado corrupto). El protocolo cooperativo de apagado es:
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
doOneUnitOfWork();
}
}, "worker");
worker.start();
// ... later, from somewhere else:
worker.interrupt();
worker.join();interrupt() activa el indicador de interrupción del trabajador. Se espera que el trabajador compruebe el indicador en puntos seguros y salga. Si el trabajador está bloqueado en sleep, wait, join, o muchas llamadas de java.nio, la llamada bloqueante lanza InterruptedException inmediatamente para que el hilo pueda reaccionar.
Si capturas InterruptedException y no quieres propagarla, la convención es volver a activar el indicador para que los llamadores más arriba en la pila aún vean la interrupción:
try { Thread.sleep(1000); }
catch (InterruptedException e) {
Thread.currentThread().interrupt(); // re-arm the flag
return; // and give up cooperatively
}Ignorar una interrupción sin reactivar el indicador es un error. El indicador es la forma en que el resto del programa sabe que se le pidió detenerse.
Un ejemplo completo: el ciclo de vida completo en un programa
El siguiente programa crea dos trabajadores de distintas formas (Runnable, subclase), observa sus transiciones de estado, los une y demuestra el protocolo de interrupción en un tercer trabajador.
Qué aprender de la ejecución:
- Las transiciones de estado coinciden con el contrato. Antes de
start(), ambos hilos eranNEW. Después destart(), eranRUNNABLE(oTERMINATEDsi el trabajo era pequeño y terminó antes del print). Después dejoin(), ambos eranTERMINATED. Ese es el ciclo de vida que describeThread.State. - La línea "t3 ran on thread: main" es el error que debes recordar para siempre.
t3.run()ejecutó el cuerpo — en el hilo que hace la llamada, de forma síncrona. No se creó ningún hilo nuevo.t3.isAlive()erafalsedespués porque nunca se llamó astart(). Si estás depurando "nada parece ejecutarse en paralelo," comprueba si escribistestart()orun(). - El bucle de interrupción no usó
Thread.sleepcomo su espera principal — simplemente comprobó el indicador continuamente, con un breve sleep ocasional para que la interrupción pudiera terminar el sleep antes de tiempo. El contrato es el mismo en cualquier caso:isInterrupted()es lo que el trabajador consulta;interrupt()es lo que llama quien lo solicita. - Reactivar el indicador dentro del
catch(la líneaThread.currentThread().interrupt()) preservó la señal para cualquier código más arriba en la pila de llamadas. Sin esa línea, una interrupción captada e ignorada desaparecería — lo que es una de las formas más fáciles de escribir un hilo que no se apagará limpiamente. - El daemon al final estaba a punto de dormirse durante 60 segundos; en cambio, la JVM salió en cuanto
mainretornó, matándolo a mitad del sleep. Los hilos daemon pueden retener cualquier tipo de recurso — pero también pueden ser cortados en cualquier momento, por eso no debes poner trabajo que requiera confirmación en ellos.
Qué viene a continuación
El siguiente capítulo, Interfaz Runnable de Java, se centra en Runnable — qué es realmente, por qué se añadieron Callable y Future sobre él, y cómo las lambdas cambiaron la ergonomía de pasar trabajo a un hilo.