Hilos con Thread y Runnable
En Java, existen dos formas principales de crear y gestionar hilos: utilizando la clase Thread o implementando la interfaz Runnable. Ambas opciones permiten ejecutar tareas de manera concurrente, pero cada una tiene sus propias ventajas y desventajas. A continuación, exploraremos cómo utilizar ambas herramientas para crear hilos en Java. Y en que consiste el ciclo de vida de un hilo, así como los métodos más comunes para gestionarlos, como sleep(), join(), e interrupt().
Creación de Hilos con la Clase Thread
Para crear un hilo utilizando la clase Thread, debes extender la clase Thread y sobrescribir el método run(), que contiene el código que se ejecutará en el hilo. Luego, puedes crear una instancia de tu clase y llamar al método start() para iniciar el hilo.
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hilo ejecutándose: " + Thread.currentThread().getName());
}
}
En este ejemplo, hemos creado una clase MyThread que extiende Thread y sobrescribe el método run(). Para iniciar el hilo, puedes hacer lo siguiente:
void main(){
MyThread thread1 = new MyThread();
thread1.start();
}
En este caso, hemos creado una instancia de MyThread y llamado al método start(), lo que iniciará el hilo y ejecutará el código dentro del método run(). Es importante destacar que no debes llamar al método run() directamente, ya que esto ejecutará el código en el hilo principal en lugar de crear un nuevo hilo.
Este enfoque es sencillo y directo, pero tiene la limitación de que no puedes extender otra clase si ya estás extendiendo Thread, lo que puede ser un problema si necesitas heredar de otra clase.
Así mismo, la clase Thread proporciona métodos adicionales para gestionar el hilo, como join(), sleep(), interrupt(), entre otros, que te permiten controlar el comportamiento del hilo de manera más precisa.
Sleep
El método sleep() es un método estático de la clase Thread que provoca que el hilo actualmente en ejecución (el que llama a sleep) suspenda temporalmente su actividad durante el período de tiempo especificado, expresado en milisegundos. El hilo no pierde la propiedad de ningún monitor que haya adquirido.
Características Principales
- Naturaleza estática: Siempre actúa sobre el hilo que lo ejecuta. Es común el error de pensar que t.sleep() hace dormir al hilo t, pero no es así; hace dormir al hilo actual.
- No libera monitores (Locks): Si el hilo que se pone a dormir tiene uno o varios bloqueos, los retiene durante todo el período de sueño. Esto puede causar problemas de rendimiento o bloqueos si no se diseña con cuidado.
- Lanza InterruptedException: Es un método "interrumpible". Si mientras el hilo duerme, otro hilo llama a su método interrupt(), este despertará inmediatamente y lanzará una excepción InterruptedException.
- Estado del Hilo: Durante su ejecución, el hilo se encuentra en el estado TIMED_WAITING (espera con tiempo definido).
- Precisión: La precisión del tiempo de espera depende del sistema operativo y del planificador de hilos del hardware subyacente.
Ejemplo de Uso
Un caso de uso común es simular una tarea que lleva tiempo, como una operación de I/O o un cálculo complejo, o para crear una pausa en la ejecución.
public class EjemploSleep {
public static void main(String[] args) {
System.out.println("Hilo main: Inicio.");
Thread tarea = new Thread(() -> {
System.out.println("Hilo secundario: Empezando a trabajar...");
try {
// Simula una tarea que dura 2 segundos
System.out.println("Hilo secundario: Me pondré a dormir por 2 segundos.");
Thread.sleep(2000); // El hilo secundario se duerme por 2000 ms
System.out.println("Hilo secundario: ¡Desperté! Tarea completada.");
} catch (InterruptedException e) {
System.out.println("Hilo secundario: Alguien me interrumpió mientras dormía.");
}
});
tarea.start();
System.out.println("Hilo main: El hilo secundario ha sido lanzado. Fin del main.");
}
}
Explicación paso a paso:
Inicio del Programa
El hilo main comienza y crea un nuevo hilo (tarea).
Creación del Hilo Secundario
main inicia la ejecución del hilo tarea con tarea.start().
Continuación del Hilo Main
El hilo main imprime su mensaje final y termina (aunque la JVM no termina hasta que todos los hilos no-daemon hayan acabado).
Ejecución del Hilo Secundario
El hilo tarea comienza su ejecución. Imprime que empieza a trabajar.
Estado de Sleep
Llega a Thread.sleep(2000). En este punto, el hilo tarea se bloquea a sí mismo y pasa al estado TIMED_WAITING. Durante estos 2 segundos, no consume CPU.
Fin del Sleep
Tras los 2 segundos, el planificador de hilos de Java lo marca como "ejecutable" (runnable). Cuando la CPU esté disponible para él, el hilo tarea se despierta y continúa con la siguiente instrucción, imprimiendo que ha despertado.
En resumen, el método sleep() es una herramienta útil para controlar la ejecución de los hilos, permitiendo introducir pausas o simular tareas que llevan tiempo. Sin embargo, es importante usarlo con cuidado para evitar problemas de rendimiento o bloqueos en la aplicación.
Join
El método join() es un método de instancia de la clase Thread. Permite que un hilo espere a que otro hilo complete su ejecución antes de continuar. Si un hilo A llama a threadB.join(), el hilo A se suspenderá hasta que el hilo B termine.
Características Principales
- Espera por finalización: Su propósito principal es la coordinación, asegurando que un hilo no prosiga hasta que otro haya muerto. Es decir, el hilo que llama a
join()se bloquea hasta que el hilo sobre el que se llamajoin()termine su ejecución. - Versiones sobrecargadas: Existen tres versiones:
join(): Espera indefinidamente hasta que el hilo muera.- join(long millis): Espera un máximo de millis milisegundos a que el hilo muera. Si pasa el tiempo, el hilo actual continúa.
- join(long millis, int nanos): Espera con una precisión de nanosegundos.
- Lanza
InterruptedException: Si mientras el hilo A espera en unjoin(), otro hilo C llama ainterrupt()sobre el hilo A, este despertará y lanzaráInterruptedException. - Mecanismo interno: Internamente,
join()utiliza wait(). Cuando un hilo termina, su objeto Thread llama a notifyAll() para despertar a todos los hilos que estén esperando en sujoin(). - Estado del Hilo: El hilo que llama a
join()pasa al estado WAITING (si esjoin()sin tiempo) oTIMED_WAITING(si es con tiempo).
Ejemplo de Uso
Ideal para descomponer un problema grande en subtareas que se ejecutan en paralelo y luego combinar sus resultados.
public class EjemploJoin {
public static void main(String[] args) {
Thread calculoComplejo = new Thread(() -> {
try {
System.out.println("Hilo cálculo: Realizando tarea pesada...");
Thread.sleep(3000); // Simula un cálculo de 3 segundos
System.out.println("Hilo cálculo: Tarea pesada terminada.");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
calculoComplejo.start();
System.out.println("Hilo main: Esperando a que el hilo de cálculo termine...");
try {
calculoComplejo.join(); // main se detiene AQUÍ hasta que calculoComplejo muera
} catch (InterruptedException e) {
System.out.println("Hilo main: Me interrumpieron mientras esperaba.");
}
System.out.println("Hilo main: El hilo de cálculo terminó. Ahora puedo procesar sus resultados y finalizar.");
}
}
En este ejemplo, el hilo main inicia un hilo llamado calculoComplejo que simula una tarea pesada. Luego, el hilo main llama a calculoComplejo.join(), lo que hace que el hilo main se bloquee hasta que el hilo calculoComplejo termine su ejecución. Una vez que calculoComplejo ha terminado, el hilo main continúa y puede procesar los resultados de la tarea pesada antes de finalizar.
Inicio del Programa
El hilo main comienza y crea un nuevo hilo (calculoComplejo).
Ejecución del Hilo de Cálculo
El hilo calculoComplejo inicia su ejecución y simula una tarea pesada con Thread.sleep(3000).
Espera del Hilo Main
Mientras calculoComplejo está "durmiendo", el hilo main llama a calculoComplejo.join(), lo que hace que el hilo main se bloquee y espere a que calculoComplejo termine.
Fin del Hilo de Cálculo
Después de 3 segundos, calculoComplejo termina su tarea y muere. Esto hace que el hilo main se despierte y continúe su ejecución.
Continuación del Hilo Main
El hilo main imprime que el hilo de cálculo terminó y puede procesar los resultados antes de finalizar.
En resumen, el método join() es una herramienta esencial para coordinar la ejecución de hilos en Java, permitiendo que un hilo espere a que otro termine antes de continuar. Es especialmente útil para descomponer tareas complejas en subtareas concurrentes y luego combinar sus resultados de manera ordenada. Sin embargo, es importante usarlo correctamente para evitar bloqueos innecesarios o errores lógicos en la aplicación.
Interrupt
El método interrupt() es un método de instancia de la clase Thread que se utiliza para interrumpir un hilo que está en ejecución. Cuando se llama a interrupt() sobre un hilo, se establece un indicador de interrupción en ese hilo. Si el hilo está bloqueado en una operación que puede ser interrumpida (como sleep(), wait(), o join()), se lanzará una excepción InterruptedException y el hilo podrá manejar esta interrupción de manera adecuada.
Características Principales
- Indicador de Interrupción: Llamar a
interrupt()no detiene inmediatamente el hilo, sino que establece un indicador de interrupción. El hilo debe verificar este indicador para responder a la interrupción. - Interrupción de Operaciones Bloqueantes: Si el hilo está bloqueado en una operación que puede ser interrumpida, como
sleep(),wait(), ojoin(), se lanzará una excepciónInterruptedException, lo que permite al hilo manejar la interrupción de manera adecuada.- No Detiene el Hilo: Si el hilo no está bloqueado en una operación interrumpible, el método
interrupt()simplemente establece el indicador de interrupción, y el hilo debe verificar este indicador para decidir cómo responder a la interrupción.
- No Detiene el Hilo: Si el hilo no está bloqueado en una operación interrumpible, el método
- Verificación del Indicador: El hilo puede verificar el indicador de interrupción utilizando el método
isInterrupted()o el método estáticoThread.interrupted(), que también limpia el indicador de interrupción. - Uso Común: El método
interrupt()se utiliza comúnmente para solicitar que un hilo se detenga de manera cooperativa, especialmente en situaciones donde el hilo está realizando una tarea que puede ser interrumpida, como esperar por recursos, dormir, o realizar operaciones de I/O. - Manejo de Interrupciones: Es importante que los hilos manejen las interrupciones de manera adecuada, ya sea limpiando recursos, cerrando conexiones, o simplemente terminando su ejecución de manera ordenada.
InterruptedException: Los métodos que lanzan esta excepción lo hacen para notificar que han sido interrumpidos mientras estaban en un estado de bloqueo. Es una señal para que el hilo finalice su tarea lo antes posible.
Ejemplo de Uso
El ejemplo clásico es un hilo que ejecuta un trabajo largo pero que debe ser capaz de cancelarse bajo demanda.
public class EjemploInterrupt {
public static void main(String[] args) {
Thread trabajador = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { // 1. Comprueba la bandera
System.out.println("Hilo trabajador: trabajando...");
try {
Thread.sleep(500); // 2. Simula trabajo que puede ser bloqueante
} catch (InterruptedException e) {
// 4. Si nos interrumpen mientras dormimos, la bandera se limpia
System.out.println("Hilo trabajador: ¡Me interrumpieron durante el sueño! Saliendo.");
Thread.currentThread().interrupt(); // Es buena práctica restaurar el estado de interrupción
break; // Sale del bucle
}
}
System.out.println("Hilo trabajador: Trabajo finalizado.");
});
trabajador.start();
try {
Thread.sleep(2000); // El main duerme 2 segundos
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hilo main: Es hora de parar al trabajador.");
trabajador.interrupt(); // 3. Se envía la señal de interrupción
}
}
En este ejemplo, el hilo trabajador ejecuta un bucle que simula trabajo. El hilo main duerme durante 2 segundos y luego llama a trabajador.interrupt(), lo que establece el indicador de interrupción en el hilo trabajador. Si el hilo trabajador está dormido en ese momento, se lanzará una excepción InterruptedException, que es manejada para salir del bucle y finalizar la ejecución del hilo de manera ordenada.
Inicio del Programa
El hilo main comienza y crea un nuevo hilo (trabajador).
Ejecución del Hilo Trabajador
El hilo trabajador inicia su ejecución y entra en un bucle donde simula trabajo con Thread.sleep(500).
Interrupción del Hilo Trabajador
Después de 2 segundos, el hilo main llama a trabajador.interrupt(), lo que establece el indicador de interrupción en el hilo trabajador.
Manejo de la Interrupción
Si el hilo trabajador está dormido en ese momento, se lanzará una excepción InterruptedException, que es manejada para salir del bucle y finalizar la ejecución del hilo de manera ordenada.
Continuación del Hilo Main
El hilo main imprime un mensaje indicando que es hora de parar al trabajador y luego finaliza su ejecución.
En resumen, el método interrupt() es una herramienta esencial para gestionar la interrupción de hilos en Java, permitiendo que un hilo solicite a otro que se detenga de manera cooperativa. Es importante que los hilos manejen las interrupciones de manera adecuada para garantizar que los recursos se liberen correctamente y que la aplicación funcione de manera estable.
Relación entre Join, Sleep e Interrupt
La relación entre estos métodos es clave:
sleep()yjoin()son métodos que ponen al hilo en un estado de espera.interrupt()es el método que se utiliza para sacar a un hilo de esos estados de espera de forma anticipada, lanzando una InterruptedException para que el hilo pueda manejar la señal de parada.
En conjunto, estos métodos permiten una gestión efectiva de la concurrencia en Java, permitiendo que los hilos se coordinen entre sí y respondan a señales de interrupción de manera adecuada. Es fundamental entender cómo funcionan estos métodos para diseñar aplicaciones concurrentes robustas y eficientes.
Creación de Hilos con la Interfaz Runnable
Otra forma de crear un hilo en Java es implementando la interfaz Runnable. Esto es generalmente preferido, ya que permite una mayor flexibilidad al no requerir la extensión de la clase Thread, lo que significa que tu clase puede extender otra clase si es necesario. Para crear un hilo utilizando Runnable, debes implementar la interfaz Runnable y sobrescribir el método run(), que contiene el código que se ejecutará en el hilo. Luego, puedes crear una instancia de tu clase Runnable y pasarla al constructor de Thread para iniciar el hilo.
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hilo ejecutándose: " + Thread.currentThread().getName());
}
}
En este ejemplo, hemos creado una clase MyRunnable que implementa la interfaz Runnable y sobrescribe el método run(). Para iniciar el hilo, puedes hacer lo siguiente:
void main(){
MyRunnable runnable = new MyRunnable();
Thread thread1 = new Thread(runnable);
thread1.start();
}
En este caso, hemos creado una instancia de MyRunnable, luego hemos creado un nuevo hilo pasando esa instancia al constructor de Thread, y finalmente hemos llamado al método start() para iniciar el hilo. Al igual que con la clase Thread, no debes llamar al método run() directamente, ya que esto ejecutará el código en el hilo principal en lugar de crear un nuevo hilo.
Este enfoque es más flexible que extender Thread, ya que permite que tu clase implemente otras interfaces o extienda otra clase si es necesario. Además, es una práctica común en Java utilizar Runnable para definir la tarea que se ejecutará en un hilo, y luego usar Thread para gestionar la ejecución de esa tarea.
Ciclo de Vida de un Hilo
Un hilo en Java pasa por varios estados a lo largo de su ciclo de vida. Estos estados son:
- Nuevo (New): El hilo se ha creado, pero aún no ha comenzado a ejecutarse. En este estado, el hilo no es elegible para la ejecución.
- Ejecutable (Runnable): El hilo está listo para ejecutarse y espera a que el planificador de hilos lo seleccione para su ejecución. En este estado, el hilo puede ser seleccionado por el planificador para ejecutarse.
- En Ejecución (Running): El hilo está actualmente en ejecución. En este estado, el hilo está utilizando la CPU para ejecutar su código.
- Bloqueado (Blocked): El hilo está bloqueado esperando a que ocurra un evento específico, como la liberación de un recurso o la finalización de otro hilo.
- Terminado (Terminated): El hilo ha completado su ejecución o ha sido interrumpido. En este estado, el hilo ya no es elegible para la ejecución.
- Esperando (Waiting): El hilo está esperando indefinidamente a que otro hilo realice una acción específica, como notificarlo o interrumpirlo.
Para poder gestionar estos estados, Java proporciona varios métodos, como sleep(), join(), e interrupt(), que permiten controlar el comportamiento de los hilos y coordinar su ejecución de manera efectiva. Lo cual hemos visto en detalle en las secciones anteriores.
Conclusión
En resumen, tanto la clase Thread como la interfaz Runnable son herramientas fundamentales para crear y gestionar hilos en Java. La elección entre ambos enfoques depende de las necesidades específicas de tu aplicación y de tu preferencia personal, pero en general, implementar Runnable es considerado una mejor práctica debido a su mayor flexibilidad.