Ejemplo con ThreadPoolExecutor

De ChuWiki


Tienes el código de este ejemplo en EjecutorExample.java

¿Por qué ThreadPoolExecutor?[editar]

Un Thread es algo relativamente costoso de crear, por lo que crear un Thread cada vez que lo necesitamos, no es algo eficiente. Es más eficiente si tenemos un conjunto de Thread ya creados y los vamos reutilizando para diversas tareas que queramos que se ejecuten en hilos separados. Para ello, tenemos la clase ThreadPoolExecutor, que contiene un número configurable de Thread a los que podremos ir pasando tareas a ejecutar.

Ejemplo con ThreadPoolExecutor[editar]

Veamos un ejemplo de código de cómo usarlo

// Se instancia en try-with-resources para asegurar el cierre de todos los hilos al terminar.
try(ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(100,100,0L,
        TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>())){

    // Se lanza un Runnable
    final Future<?> runnableResult = threadPoolExecutor.submit(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Finalizado Submit de Runnable");
    });
    // Se espera por el resultado y se saca por pantalla, que será null
    System.out.println(runnableResult.get());

    // Se lanza un Callable
    final Future<Double> callableResult = threadPoolExecutor.submit(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Finalizado Submit de Callable");
        return Math.random();
    });
    // Se espera por el resultado y se saca por pantalla
    System.out.println(callableResult.get());

} catch (ExecutionException e) {
    e.printStackTrace();
} catch (InterruptedException e) {
    e.printStackTrace();
}

Instanciar ThreadPoolExecutor[editar]

Hemos instanciado ThreadPoolExecutor dentro de una estructura try-with-resources para asegurarnos del cierre de los hilos una vez hayamos terminado de usarlos. Los parámetros que admite el constructor, en el orden que están en el código:

  • Número core de hilos. Cuántos hilos queremos arrancados de forma permanente, incluso aunque estén desocupados. En el ejemplo, hemos puesto 10
  • Número máximo de hilos. En caso de muchas tareas asignadas, se podrán crear más hilos hasta este máximo. Hemos indicado 100. Nuestro pool de hilos tendrá entre 10 y 100, según la carga de trabajo.
  • Tiempo idle. Este tiempo indica cuanto tiempo puede estar desocupado uno de los hilos adicionales. Si pasa ese tiempo desocupado, se mata el hilo. No afecta a los 10 hilos del core, que siempre estarán creados aunque estén desocupados. Hemos puesto como tiempo de idle 0.
  • Unidad de tiempo para el parámetro anterior. Indicamos milisegundos, por lo que el tiempo idle será 0 milisegundos
  • Una BlockingQueue de Runnable donde se guardarán las tareas que queremos asignar a los hilos mientras no haya hilos disponibles.

No obstante, no es necesario instanciar con todos estos parámetros. La clase Executors tiene métodos que nos permiten instanciar ThreadPoolExecutor de forma más sencilla. Por ejemplo

try(ExecutorService executorService = Executors.newFixedThreadPool(10)) {
   ...
}

nos devuelve un ExecutorService que realmente es una instancia de ThreadPoolExecutor con 10 hilos para reutilizar, equivalente a poner por defecto los demás parámetros, de la siguiente forma

new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

número máximo de hilos igual al número core de hilos, diez en nuestro ejemplo y tiempo de idle de 0 milisegundos.

ThreadPoolExecutor submit[editar]

Una vez creada la instancia de ThreadPoolExecutor, sólo debemos llamar al método submit() pasándole un Runnable o un Callable. El primero es una tarea que no devuelve resultado, el segundo es una tarea que devuelve un resultado cuando termina.

En el primer bloque hemos lanzado un Runnable. Simplemente una espera y sacar por pantalla un texto.

En el segundo bloque hemos lanzado un Callable que devuelve un resultado. Simplemente una espera y devolver un número aleatorio. El restultado nos lo devuelve submit() como un Future<Double>. La llamada a get() para obtener el resultado se queda bloqueada hasta que el resultado esté disponible.

ThreadPoolExecutor submit vs execute[editar]

Si te fijas, ambas llamadas a submit() devuelven un Future<>. Hemos comentado en el caso de Callable que ahí estará el resultado y que se obtiene con get(). En el caso de Runnable, la llamda a get() devuelve null, puesto que no hay resultado.

Este Future tiene además otros métodos útiles para control de la tarea. Por ejemplo, isDone() para saber si la tarea ha terminado o cancel() para cancelar la ejecución de la misma. Ejemplos

if (future.isDone()) {
   double result = future.get();
}
if (!future.isDone()) {
   future.cancel();
}

También podemos poner un timeout para esperar por el resultado

try {
   String resultado = future.get(1,TimeUnit.SECONDS);
} catch (TimeoutException e) {
   // Se ha pasado el tiempo sin obtener resultado
}


En vez de submit(), podemos llamar a execute(), que admite un Runnable y no devuelve nada. Es más directo, pero no nos da control ninguno una vez lanzada la tarea. Si la tarea devuelve un resultado o queremos saber si ha terminado, debemos hacer nuestra propia clase Runnable que nos permita obtener esta información.

Executors y ExecutorService[editar]

Si no queremos el deatalle de todos los parámetros y símplemente queremos un número de hilos fijo con todos los demás parámetros por defecto, podemos obetener la instancia de ThreadPoolExcecutor a partir de la clase Executors, de la siguiente forma

try(ExecutorService executor = Executors.newFixedThreadPool(10)) {
   ...
}

siendo 10 el número de hilos que queremos que tenga preparados la clase dentro. Este método nos devuelve una interface ExecutorService, pero en realidad es una instancia de ThreadPoolExcecutor.

La interface tiene el método submit() que podemos usar para enviar Runnable o Callable, igual que hemos visto en el ejemplo anterior.

Utilidades de ExecutorService[editar]

Como executor es en realidad un ExecutorService, tenemos ciertos métodos para controlar los hilos que están dentro. Mencionamos sólo por encima algunos de ellos

  • invokeAll() permite pasar una lista de Callable para lanzar varias tareas de golpe, en una sola llamada.
  • shutdown(). Este método indica al executor que no acepte más tareas. No termina las que están en ejecución, sino que se las deja terminar de forma natural, incluyendo a las que están en cola de espera por un hilo disponible. Cuando todas terminen de forma natural, se matan los hilos y se finaliza totalmente el executor. Es importante llamar a este método cuando terminemos de usar el executor, puesto que si no, los hilos quedarán vivos.
  • shutdownNow(). Este método interrumpe todos los hilos que están en ejecución. Ya es cuestión de cada una de nuestras tareas el cómo traten esa interrupción para terminar de forma adecuada. También elimina de la cola todas las tareas que no hayan empezado a ejecutarse todavía. Al depender de cómo se comporten nuestras tareas ante una interrupción, no hay ninguna garantía de que los hilos realmente mueran o terminen.
  • awaitTermination(). Este método, después de una llamada a shutdown(), se queda bloqueado a la espera de que todos los hilos terminen. Se le pasa como parámetro el tiempo máximo de espera. Devuelve true si todos los hilos han terminado, false si ha saltado el tiempo de espera sin que hayan terminado los hilos.