La interfaz Java ExecutorService, java.util.concurrent.ExecutorService, representa un mecanismo de ejecución asíncrono que es capaz de ejecutar tareas de forma concurrente en segundo plano. En este tutorial de Java ExecutorService explicaré cómo crear un ExecutorService, cómo enviar tareas para que se ejecuten en él, cómo ver los resultados de esas tareas y cómo volver a cerrar el ExecutorService cuando sea necesario.

Video tutorial de Java ExecutorService

Si prefieres el video, tengo un video de introducción al aquí:

Vídeo tutorial de Java ExecutorService - Parte 1
Vídeo tutorial de Java ExecutorService - Parte 2

Delegación de tareas

Aquí tienes un diagrama que ilustra un hilo que delega una tarea a un Java ExecutorService para su ejecución asíncrona:

Un hilo que delega una tarea a un ExecutorService para su ejecución asíncrona.

Un hilo que delega una tarea a un ExecutorService para su ejecución asíncrona.

Una vez que el hilo ha delegado la tarea al ExecutorService, el hilo continúa su propia ejecución independientemente de la ejecución de esa tarea. El ExecutorService ejecuta entonces la tarea de forma concurrente, independientemente del hilo que envió la tarea.

Ejemplo de Java ExecutorService

Antes de profundizar en el ExecutorService, veamos un ejemplo sencillo. Aquí tenemos un sencillo ejemplo de Java ExecutorService:

ExecutorService executorService = Executors.newFixedThreadPool(10);executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();

Primero se crea un ExecutorService utilizando el método de fábrica ExecutorsnewFixedThreadPool(). Esto crea un pool de hilos con 10 hilos ejecutando tareas.

En segundo lugar, se pasa una implementación anónima de la interfaz Runnable al método execute(). Esto hace que el Runnable sea ejecutado por uno de los hilos del ExecutorService.

Verás varios ejemplos más de cómo utilizar el ExecutorService a lo largo de este tutorial. Este ejemplo sólo ha servido para darte una visión rápida de cómo es el uso de un ExecutorService para ejecutar tareas en segundo plano.

Implementaciones del ExecutorService de Java

El ExecutorService de Java es muy similar a un thread pool. De hecho, la implementación de la interfaz ExecutorService presente en el paquete java.util.concurrent es una implementación de thread pool. Si quieres entender cómo se puede implementar internamente la interfaz ExecutorService, lee el tutorial anterior.

Como ExecutorService es una interfaz, necesitas sus implementaciones para poder hacer cualquier uso de ella. El ExecutorService tiene la siguiente implementación en el paquete java.util.concurrent:

  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

Crear un ExecutorService

La forma de crear un ExecutorService depende de la implementación que utilices. Sin embargo, puedes usar la clase de fábrica Executors para crear instancias de ExecutorService también. Aquí hay algunos ejemplos de creación de un ExecutorService:

ExecutorService executorService1 = Executors.newSingleThreadExecutor();ExecutorService executorService2 = Executors.newFixedThreadPool(10);ExecutorService executorService3 = Executors.newScheduledThreadPool(10);

Uso del ExecutorService

Hay algunas formas diferentes de delegar tareas para su ejecución a un ExecutorService:

  • ejecutar(Runnable)
  • someter(Runnable)
  • someter(Callable)
  • invocarAny(…)
  • invocarTodo(…)
  • En las siguientes secciones echaré un vistazo a cada uno de estos métodos.

    Ejecutar Runnable

    El método Java ExecutorServiceexecute(Runnable) toma un objeto java.lang.Runnable y lo ejecuta de forma asíncrona. A continuación se muestra un ejemplo de ejecución de un Runnable con un ExecutorService:

ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();

No hay forma de obtener el resultado del Runnable ejecutado, si es necesario. Tendrás que utilizar un Callable para ello (se explica en los siguientes apartados).

Subir Runnable

El método Java ExecutorServicesubmit(Runnable) también toma una implementación Runnable, pero devuelve un objeto Future. Este objeto Future puede utilizarse para comprobar si el Runnable ha terminado de ejecutarse.

Aquí hay un ejemplo de Java ExecutorServicesubmit():

Future future = executorService.submit(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});future.get(); //returns null if the task has finished correctly.

El método submit() devuelve un objeto Java Future que se puede utilizar para comprobar cuándo se ha completado el Runnable.

Subir Callable

El método Java ExecutorServicesubmit(Callable) es similar al método submit(Runnable) excepto que toma un Callable Java en lugar de un Runnable. La diferencia precisa entre un Callable y un Runnable se explica un poco más adelante.

El resultado del Callable se puede obtener a través del objeto Java Future devuelto por el método submit(Callable). Aquí hay un ExecutorServiceCallable ejemplo:

Future future = executorService.submit(new Callable(){ public Object call() throws Exception { System.out.println("Asynchronous Callable"); return "Callable Result"; }});System.out.println("future.get() = " + future.get());

El ejemplo de código anterior dará como resultado esto:

Asynchronous Callablefuture.get() = Callable Result

invocarAny()

El método invokeAny() toma una colección de objetos Callable, o subinterfaces de Callable. Invocar este método no devuelve un Future, sino que devuelve el resultado de uno de los objetos Callable. No tiene ninguna garantía sobre cuál de los resultados del Callable obtiene. Sólo uno de los que terminan.

Si uno de los Callables termina, de forma que se devuelve un resultado de invokeAny(), el resto de las instancias de Callable se cancelan.

Si una de las tareas termina (o lanza una excepción), el resto de los Callable se cancelan.

Aquí tienes un ejemplo de código:

ExecutorService executorService = Executors.newSingleThreadExecutor();Set<Callable<String>> callables = new HashSet<Callable<String>>();callables.add(new Callable<String>() { public String call() throws Exception { return "Task 1"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 2"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 3"; }});String result = executorService.invokeAny(callables);System.out.println("result = " + result);executorService.shutdown();

Este ejemplo de código imprimirá el objeto devuelto por uno de los Callable de la colección dada. He probado a ejecutarlo varias veces y el resultado cambia. A veces es «Tarea 1», a veces «Tarea 2», etc.

invocarTodo()

El método invokeAll() invoca todos los objetos Callable que le pasas en la colección pasada como parámetro. El invokeAll() devuelve una lista de objetos Future a través de la cual puedes obtener los resultados de las ejecuciones de cada Callable.

Ten en cuenta que una tarea puede terminar debido a una excepción, por lo que puede no haber «tenido éxito». No hay manera en un Future de distinguir la diferencia.

Aquí hay un ejemplo de código:

ExecutorService executorService = Executors.newSingleThreadExecutor();Set<Callable<String>> callables = new HashSet<Callable<String>>();callables.add(new Callable<String>() { public String call() throws Exception { return "Task 1"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 2"; }});callables.add(new Callable<String>() { public String call() throws Exception { return "Task 3"; }});List<Future<String>> futures = executorService.invokeAll(callables);for(Future<String> future : futures){ System.out.println("future.get = " + future.get());}executorService.shutdown();

Runable vs. Callable Callable

La interfaz Runnable es muy similar a la interfaz Callable. La interfaz Runnable representa una tarea que puede ser ejecutada concurrentemente por un hilo o un ExecutorService. El Callable sólo puede ser ejecutado por un ExecutorService. Ambas interfaces sólo tienen un único método. Hay una pequeña diferencia entre la interfaz Callable y Runnable. La diferencia entre la interfaz Runnable y Callable es más fácilmente visible cuando ves las declaraciones de la interfaz.

Aquí está primero la declaración de la interfaz Runnable:

public interface Runnable { public void run();}

Y aquí está la declaración de la interfaz Callable:

public interface Callable{ public Object call() throws Exception;}

La principal diferencia entre el método Runnablerun() y el Callablecall() es que el método call() puede devolver un Object de la llamada al método. Otra diferencia entre call() y run() es que call() puede lanzar una excepción, mientras que run() no puede (excepto para las excepciones no comprobadas – subclases de RuntimeException).

Si necesitas enviar una tarea a un ExecutorService de Java y necesitas un resultado de la tarea, entonces tienes que hacer que tu tarea implemente la interfaz Callable. De lo contrario, tu tarea puede implementar simplemente la interfaz Runnable.

Cancelar tarea

Puedes cancelar una tarea (Runnable o Callable) enviada a un ExecutorService llamando al método cancel() en el Future devuelto cuando se envía la tarea. La cancelación de la tarea sólo es posible si la tarea aún no ha comenzado a ejecutarse. A continuación se muestra un ejemplo de cancelación de una tarea llamando al método Future.cancel():

future.cancel();

Apagado del ExecutorService

Cuando termines de utilizar el ExecutorService de Java debes apagarlo, para que los hilos no sigan ejecutándose. Si tu aplicación se inicia a través de un método main() y tu hilo principal sale de tu aplicación, ésta seguirá funcionando si tienes un ExexutorService activo en tu aplicación. Los hilos activos dentro de este ExecutorService evitan que la JVM se apague.

shutdown()

Para terminar los hilos dentro del ExecutorService se llama a su método shutdown(). El ExecutorService no se cerrará inmediatamente, pero ya no aceptará nuevas tareas, y una vez que todos los hilos hayan terminado las tareas actuales, el ExecutorService se cierra. Todas las tareas enviadas al ExecutorService antes de que se llame a shutdown() se ejecutan. A continuación se muestra un ejemplo de realización de un apagado Java ExecutorService:

executorService.shutdown();

apagarAhora()

Si quieres apagar el ExecutorService inmediatamente, puedes llamar al método shutdownNow(). Esto intentará detener todas las tareas en ejecución de inmediato, y omitirá todas las tareas enviadas pero no procesadas. No se dan garantías sobre las tareas en ejecución. Quizás se detengan, quizás se ejecuten hasta el final. Es un intento de mejor esfuerzo. Aquí hay un ejemplo de llamada ExecutorServiceshutdownNow:

executorService.shutdownNow();

esperarTerminación()

El método ExecutorServiceawaitTermination() bloqueará el hilo que lo llama hasta que el ExecutorService se haya apagado completamente, o hasta que se produzca un tiempo de espera determinado. El método awaitTermination() suele llamarse después de llamar a shutdown() o shutdownNow(). Este es un ejemplo de llamada a ExecutorServiceawaitTermination():

executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *