5 Interrumpir¶
Interrumpir un hilo¶
Como hemos visto, un programa Java con más de un hilo de ejecución finaliza solamente cuando ha finalizado la ejecución de todos sus hilos (que no sean demonios) o cuando uno de sus hilos usa el método System.exit()
.
Algunas veces, sin embargo, es necesario finalizar un hilo concreto, ya sea porque se quiera cancelar la tarea que el hilo está llevando a cabo o porque sea el único hilo que queda en ejecución y queramos finalizar el programa.
Para ello Java proporciona un mecanismo de interrupción para indicar a un hilo que queremos que finalice su ejecución. Este mecanismo tiene la peculiaridad de que es el propio hilo el que debe comprobar si ha sido interrumpido o no, y decidir si aceptar la solicitud de finalización o ignorarla y seguir con su ejecución, aunque no es el comportamiento que se espera de él. Si decide finalizar, simplemente retornará (hará return
) del método run()
.
La clase Thread
tiene un atributo que almacena un valor booleano que indica si el hilo ha sido interrumpido o no (flag o bandera). Cuando se llama al método interrupt()
de un hilo, se establece dicho atributo a true
. Podemos usar el método isInterrupted()
para conocer el valor de dicho atributo.
Nunca hagas esto
Nunca uses el método stop()
de la clase Thread
La clase Thread
dispone de un método stop()
, que fuerza que un hilo deje de ejecutarse inmediatamente. Sin embargo, esta forma de actuar es totalmente insegura, ya que no permite al hilo liberar antes de terminar su ejecución los recursos que hubiera adquirido. Por tanto no se recomienda bajo ninguna circunstancia el uso de el método stop()
, y de hecho se trata de un método deprecated (obsoleto).
Existe otro método en la clase Thread
, en este caso estático, llamado interrupted()
que retorna si el hilo que se está ejecutando ha sido interrumpido o no, y establece dicho atributo a false
.
La diferencia entre isInterrupted()
y Thread.interrupted()
es que este último borra el estado de interrupción del hilo, mientras que el otro no.
Si el código que comprueba el estado de la interrupción es el "propietario" del hilo, en ciertos casos muy concretos puede ser apropiado borrar la bandera de interrumpido, siempre y cuando el propietario está implementando la política de cancelación del hilo.
Tips
En general se prefiere usar isInterrupted()
frente a Thread.interrupted()
.
Sin embargo, en la gran mayoría de los casos, incluyendo cuando el código ejecutado por el hilo corresponde a un Runnable, el código en cuestión no es el propietario del hilo y por tanto no debe borrar la bandera de interrupción, dado que no es quien implementa la política de cancelación.
Por tanto, dentro del método run()
del hilo deberemos comprobar periódicamente el valor de isInterrupted()
para comprobar si se ha solicitado la interrupción del hilo.
public class MyRunnable implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// ...
}
}
}
Un aspecto muy importante de la interrupción es que si el hilo trata de ejecutar alguna operación bloqueante (que vaya a bloquear el hilo), como por ejemplo las operaciones wait()
, join()
, sleep()
(que veremos más adelante), o las llamadas de E/S bloqueantes, y dicho hilo tiene activado el valor booleano de interrupción, la llamada a dicho método lanzará automáticamente la excepción InterruptedException
, por lo que deberemos capturar la interrupción o hacer que el método dicha excepción.
Igualmente, si el hilo está bloqueado en alguna de estas operaciones bloqueantes y se le activa el valor booleano de interrupción desde otro hilo, el hilo que estaba bloqueado será desbloqueado inmediatamente y se le lanzará automáticamente la excepción InterruptedException
.
public class MyRunnable implements Runnable {
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
// ...
try {
Thread.sleep(sleepingTime);
} catch (InterruptedException e) {
// The thread has been interrupted
return;
}
}
}
}
Cuando el código de un hilo no es sencillo, como por ejemplo en el caso de que sea recursivo, el método descrito anteriormente de capturar directamente la interrupción del hilo puede llegar a ser complejo. En estos casos es mejor opción dejar que el propio método lance la excepción InterruptedException
cuando se detecte que se el hilo ha sido marcado como interrumpido, propagando la excepción hacia el llamador. Al final de la cadena de llamadas, en el método run()
capturaremos la excepción.
public class MyRunnable implements Runnable {
@Override
public void run() {
try {
recursiveMethod(initialValue);
} catch (InterruptedException e) {
// The thread has been interrupted
return;
}
}
private void recursiveMethod(int value) throws InterruptedException {
// ...
}
}
En general, debemos decidir qué hacer cuando detectamos que el hilo ha sido marcado para interrupción. Tenemos varias opciones:
- Propagar la excepción
InterruptedException
simplemente no capturándola. - Capturar la excepción, realizar operaciones de liberaciones de recursos obtenidos por el hilo y después relanzar la excepción.
- Capturar la excepción y terminar.
- Capturar la excepción, restaurar el indicador de interrupción y terminar.
Proyecto Interrupt¶
En este proyecto desarrollaremos un programa que crea un hilo dedicado a mostrar por pantalla números primeros. Transcurridos unos segundos el hilo principal solicita al hilo impresor de números primos que finalice usando el mecanismo de interrupción descrito anteriormente.
public class PrimeNumberPrinter implements Runnable {
@Override
public void run() {
// Print prime numbers until someone interrupts the thread.
for (long i = 1L; !Thread.currentThread().isInterrupted(); i++) {
if (isPrimeNumber(i)) {
System.out.printf("%d is a prime number\n", i);
// If the thread is interrupted while sleeping
// InterruptedException will be thrown.
try {
Thread.sleep(50);
} catch (InterruptedException e) {
System.out.println("I've been interrupted while sleeping");
return;
}
}
}
System.out.println("I've been interrupted");
}
private boolean isPrimeNumber(long number) {
if (number < 1) {
throw new IllegalArgumentException();
}
for (long i = 2; i * i < number; i++) {
if (number % i == 0) {
return false;
}
}
return true;
}
}
Si ejecutamos el proyecto veremos que tan sólo se imprimen números durante dos segundos, dado que transcurrido dicho tiempo el hilo principal interrumpe el hilo secundario.
Proyecto InterruptedRecursiveFactorial¶
En este proyecto desarrollaremos un programa que crea un hilo dedicado a mostrar por pantalla el factorial de los número naturales. Dicho hilo usa una función recursiva para calcular el factorial. Transcurridos unos segundos el hilo principal solicita al hilo impresor de factoriales que finalice usando el mecanismo de interrupción descrito anteriormente.
public class FactorialPrinter implements Runnable {
@Override
public void run() {
// Print factorials until it is interrupted
for (int i = 1; !Thread.currentThread().isInterrupted(); i++) {
try {
System.out.printf("factorial(%d) = %d\n", i, factorial(i));
} catch (InterruptedException e) {
System.out.println("I've been interrupted");
return;
}
}
}
private int factorial(int number) throws InterruptedException {
if (number < 0) {
throw new IllegalArgumentException();
}
Thread.sleep(50);
if (number == 0 || number == 1) {
return 1;
} else {
return number * factorial(number - 1);
}
}
}
Si ejecutamos el proyecto veremos que tan sólo se imprimen factoriales durante dos segundos, dado que transcurrido dicho tiempo el hilo principal interrumpe el hilo secundario.