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í:
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.
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 Executors
newFixedThreadPool()
. 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 ExecutorService
execute(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 ExecutorService
submit(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 ExecutorService
submit()
:
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 ExecutorService
submit(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 ExecutorService
Callable
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 Runnable
run()
y el Callable
call()
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 ExecutorService
shutdownNow
:
executorService.shutdownNow();
esperarTerminación()
El método ExecutorService
awaitTermination()
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 ExecutorService
awaitTermination()
:
executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );