header






Manejo de excepciones en hilos Java

Autor: Besökare Publicación en: 2017-02-05 00:00:00 Ultima actualización con fecha: 2017-05-18 14:43:48

Cómo evitar que los hilos en Java mueran sin avisar

Cuando una línea de código Java lanza una excepción Exception o un error Error, éste se propaga hacia arriba en la pila de métodos llamados hasta que es capturado en un bloque catch o bien no es capturado y el programa detiene su ejecución. La excepción es mostrada en la salida de error estándar y el programador puede comprobar qué es lo que ha fallado.

Si se usan hilos de ejecución Thread, estos también pueden lanzar excepciones y detener su ejecución mientras que el resto de hilos, incluido el principal, siguen su curso sin recibir ningún aviso al respecto. En este caso la excepción detiene el hilo en el que ha sido lanzada pero no se muestra en la salida del programa, con lo que el programador puede observar fallos en la ejecución del programa sin saber muy bien el porqué.

Para estos casos, la clase Thread es capaz de establecer una función que maneje debidamente cualquier excepción no capturada. La función viene definida como uncaughtException(Thread t, Throwable e) y se encuentra en la interfaz Thread.UncaughtExceptionHandler.

Es tarea nuestra decidir qué hacer con la excepción y establecer que los hilos que lancemos usen este UncaughtExceptionHandler:

Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println(t.getName() + “ “ + e.getStackTrace());
    }
});

También podemos establecer un UncaughtExceptionHandler hilo a hilo:

Thread myThread = new Thread(/*Runnable*/);
myThread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler(){
    @Override
    public void uncaughtException(Thread t, Throwable e){
        System.out.println(t.getName() + " " + e.getStackTrace ());
    }
});

Lo cual es muy habitual cuando obtenemos hilos a partir de un ThreadFactory:

class MyThreadFactory implements ThreadFactory{
    public Thread newThread(Runnable r){
        Thread myThread = new Thread(r);
        myThread.setUncaughtExceptionHandler(/*UncaughExceptionHandler*/);
        return t;
    }
}

Un problema viene cuando usamos ese ThreadFactory como argumento para un ExecutorService como puede ser un ThreadPoolExecutor:

int corePoolSize = 1, maxPoolSize = 4;
long keepAliveTime = 10;
TimeUnit timeUnit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
MyThreadFactory myThreadFactory = new MyThreadFactory();
ThreadPoolExecutor myPool = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, timeUnit, workQueue, myThreadFactory);
myPool.submit(/*Runnable*/);
myPool.submit(/*Callable*/);

A medida que el ThreadPoolExecutor vaya obteniendo hilos del myThreadFactory que le hemos proporcionado, estos estarán automáticamente ligados al UncaughtExceptionHandler que hemos establecido anteriormente en myThreadFactory.newThread(). Sin embargo, las excepciones lanzadas en estos hilos no llegarán a invocar el método uncaughtException(Thread t, Throwable e). La razón está en que las tareas Runnable y Callable que proporcionamos al ThreadPoolExecutor mediante submit() son encapsuladas internamente en objetos FutureTask, los cuales se encargan de capturar y almacenar las excepciones que estas tareas puedan lanzar. De esta manera, las excepciones no llegan a nuestro método estrella uncaughtException, al contrario que los hilos que ejecutemos fuera del ThreadPoolExecutor.

Esto se puede remediar sobreescribiendo el método afterExecute(Runnable r, Throwable t), que se ejecuta después de que cada tarea finalice, bien limpiamente o con una excepción:

class ExtendedExecutor extends ThreadPoolExecutor{
    ExtendedExecutor(/*params*/){/*Call your favourite constructor super(params)*/}
    @Override
    protected void afterExecute(Runnable r, Throwable t){
        System.out.println(t.getStackTrace());
    }
}

Aún hay que apurar algo más, ya que las excepciones que llegan a este método no son necesariamente todas las que lanza nuestra tarea. Según la documentación de Java:

protected void afterExecute(Runnable r, Throwable t)

(...) If non-null, the Throwable is the uncaught RuntimeException or Error that caused execution to terminate abruptly. (...)

Es decir, el resto de excepciones que no sean Error o RuntimeException, no se obtienen como t. La propia documentación nos dice cómo hemos de implementar este método para obtener todas las excepciones:

Note: When actions are enclosed in tasks (such as FutureTask) either explicitly or via methods such as submit, these task objects catch and maintain computational exceptions, and so they do not cause abrupt termination, and the internal exceptions are not passed to this method. If you would like to trap both kinds of failures in this method, you can further probe for such cases, as in this sample subclass that prints either the direct cause or the underlying exception if a task has been aborted:

class ExtendedExecutor extends ThreadPoolExecutor {
    // ...
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                Object result = ((Future<?>) r).get();
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt(); // ignore/reset
            }
        }
        if (t != null){
            System.out.println(t);
        }
    }
}

Si vemos que afterExecute no recibe ninguna excepción (evaluando si t==null), hay que comprobar si la tarea ha almacenado alguna por su cuenta. Como las tareas se encapsulan internamente en FutureTask, podemos convertir con un cast el Runnable r en un Future future del que obtener las excepciones mediante future.get() y capturarlas con catch.

Usando el UncaughtExceptionHandler de la clase Thread junto con el método sobreescrito afterExecute de nuestro ThreadPoolExecutor, todos los hilos, los independientes y los que maneja el ThreadPoolExecutor, enviarán sus excepciones a lugares visibles para que cuando se cuelguen (si lo hicieren), no se pierdan en el olvido sin hacer ruido y dejen al menos una nota en la nevera diciendo que las cosas no han ido bien, que tuvieron que marcharse repentinamente y que le desean al programador fuerza para rehacerse y seguir adelante.

Referencias

https://stackoverflow.com/questions/1838923/why-is-uncaughtexceptionhandler-not-called-by-executorservice

https://stackoverflow.com/questions/2248131/handling-exceptions-from-java-executorservice-tasks

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html


2017-02-05 00:00:00

0No hay comentarios

Parece que nadie ha comentado aún esta entrada.

Publicar comentario:

Complete el formulario para publicar su comentario.

Nombre de usuario:
Correo electrónico: *

* El correo electrónico se usará para enviarle un enlace de confirmación antes de publicar el comentario. Si introduce una dirección equivocada o a la que no tenga acceso, no podrá seguir el enlace y por tanto, no verá su comentario publicado. Los comentarios sin confirmación se eliminarán pasadas 4 horas.

Mensaje: