¿Por qué necesita la interfaz Executor?

Antes de Java 5, tenía que escribir su propia gestión de subprocesos de código en su aplicación. Además, crear un nuevo hilo object es una operación que consume muchos recursos y no tiene sentido crear un nuevo subproceso para cada tarea ligera. Y debido a que este problema es familiar para absolutamente todos los desarrolladores de aplicaciones de subprocesos múltiples, decidieron llevar esta funcionalidad a Java como el marco Executor .

¿Cúal es la gran idea? Es simple: en lugar de crear un nuevo hilo para cada nueva tarea, los hilos se guardan en una especie de "almacenamiento", y cuando llega una nueva tarea, recuperamos un hilo existente en lugar de crear uno nuevo.

Las principales interfaces de este marco son Executor , ExecutorService y ScheduledExecutorService , cada una de las cuales amplía la funcionalidad de la anterior.

La interfaz Executor es la interfaz base. Declara un único método de ejecución nulo (comando ejecutable) que se implementa mediante un objeto ejecutable .

La interfaz ExecutorService es más interesante. Tiene métodos para administrar la finalización del trabajo, así como métodos para devolver algún tipo de resultado. Echemos un vistazo más de cerca a sus métodos:

Método Descripción
anular el apagado (); Llamar a este método detiene ExecutorService . Se completarán todas las tareas que ya se hayan enviado para su procesamiento, pero no se aceptarán tareas nuevas.
List<Ejecutable> shutdownNow();

Llamar a este método detiene ExecutorService . Se llamará a Thread.interrupt para todas las tareas que ya se hayan enviado para su procesamiento. Este método devuelve una lista de tareas en cola.

El método no espera a que se completen todas las tareas que están "en curso" en el momento en que se llama al método.

Advertencia: Llamar a este método puede perder recursos.

booleano isShutdown(); Comprueba si ExecutorService está detenido.
booleano estáTerminado(); Devuelve verdadero si todas las tareas se completaron después del cierre de ExecutorService . Hasta que se llame a shutdown() o shutdownNow() , siempre devolverá false .
awaitTermination booleano (tiempo de espera prolongado, unidad de unidad de tiempo) lanza una excepción interrumpida;

Después de llamar al método shutdown() , este método bloquea el subproceso en el que se llama, hasta que se cumpla una de las siguientes condiciones:

  • todas las tareas programadas están completas;
  • el tiempo de espera pasado al método ha transcurrido;
  • el hilo actual se interrumpe.

Devuelve verdadero si todas las tareas están completas y falso si transcurre el tiempo de espera antes de la terminación.

<T> Futuro<T> enviar (tarea Callable<T>);

Agrega una tarea invocable a ExecutorService y devuelve un objeto que implementa la interfaz Future .

<T> es el tipo del resultado de la tarea pasada.

<T> Future<T> enviar (tarea ejecutable, resultado T);

Agrega una tarea Runnable a ExecutorService y devuelve un objeto que implementa la interfaz Future .

El parámetro de resultado T es lo que devuelve una llamada al método get() en el resultado Objeto futuro.

Future<?> enviar (tarea ejecutable);

Agrega una tarea Runnable a ExecutorService y devuelve un objeto que implementa la interfaz Future .

Si llamamos al método get() en el objeto Future resultante , obtenemos un valor nulo.

<T> List<Future<T>> invoqueTodos(Colección<? extiende las tareas Callable<T>>) lanza InterruptedException;

Pasa una lista de tareas invocables a ExecutorService . Devuelve una lista de Futuros de los que podemos obtener el resultado del trabajo. Esta lista se devuelve cuando se completan todas las tareas enviadas.

Si la colección de tareas se modifica mientras se ejecuta el método, el resultado de este método no está definido.

<T> List<Future<T>> invoqueTodos(Colección<? extiende las tareas Callable<T>>, tiempo de espera prolongado, unidad TimeUnit) lanza una excepción interrumpida;

Pasa una lista de tareas invocables a ExecutorService . Devuelve una lista de Futuros de los que podemos obtener el resultado del trabajo. Esta lista se devuelve cuando se completan todas las tareas pasadas o después de que haya transcurrido el tiempo de espera pasado al método, lo que suceda primero.

Si transcurre el tiempo de espera, las tareas inconclusas se cancelan.

Nota: Es posible que una tarea cancelada no deje de ejecutarse (veremos este efecto secundario en el ejemplo).

Si la colección de tareas se modifica mientras se ejecuta el método, el resultado de este método no está definido.

<T> T invoqueAny(Colección<? extiende tareas Callable<T>>) lanza InterruptedException, ExecutionException;

Pasa una lista de tareas invocables a ExecutorService . Devuelve el resultado de una de las tareas (si las hay) que se completó sin generar una excepción (si las hubo).

Si la colección de tareas se modifica mientras se ejecuta el método, el resultado de este método no está definido.

<T> T invoqueAny(Colección<? Extiende tareas Callable<T>>, tiempo de espera prolongado, unidad TimeUnit) lanza InterruptedException, ExecutionException, TimeoutException;

Pasa una lista de tareas invocables a ExecutorService . Devuelve el resultado de una de las tareas (si las hay) que se completó sin generar una excepción antes de que transcurriera el tiempo de espera del método.

Si la colección de tareas se modifica mientras se ejecuta el método, el resultado de este método no está definido.

Veamos un pequeño ejemplo de cómo trabajar con ExecutorService .


import java.util.List;
import java.util.concurrent.*;

public class ExecutorServiceTest {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
//Create an ExecutorService for 2 threads
       java.util.concurrent.ExecutorService executorService = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
// Create 5 tasks
       MyRunnable task1 = new MyRunnable();
       MyRunnable task2 = new MyRunnable();
       MyRunnable task3 = new MyRunnable();
       MyRunnable task4 = new MyRunnable();
       MyRunnable task5 = new MyRunnable();

       final List<MyRunnable> tasks = List.of(task1, task2, task3, task4, task5);
// Pass a list that contains the 5 tasks we created
       final List<Future<Void>> futures = executorService.invokeAll(tasks, 6, TimeUnit.SECONDS);
       System.out.println("Futures received");

// Stop the ExecutorService
       executorService.shutdown();

       try {
           TimeUnit.SECONDS.sleep(3);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }

       System.out.println(executorService.isShutdown());
       System.out.println(executorService.isTerminated());
   }

   public static class MyRunnable implements Callable<Void> {

       @Override
       public void call() {
// Add 2 delays. When the ExecutorService is stopped, we will see which delay is in progress when the attempt is made to stop execution of the task
           try {
               TimeUnit.SECONDS.sleep(3);
           } catch (InterruptedException e) {
               System.out.println("sleep 1: " + e.getMessage());
           }
           try {
               TimeUnit.SECONDS.sleep(2);
           } catch (InterruptedException e) {
               System.out.println("sleep 2: " + e.getMessage());
           }
           System.out.println("done");
           return null;
       }
   }
}

Producción:

hecho
hecho
Futuros recibidos
sueño 1: sueño interrumpido
sueño 1: sueño interrumpido
hecho
hecho
verdadero
verdadero

Cada tarea se ejecuta durante 5 segundos. Creamos un grupo para dos subprocesos, por lo que las dos primeras líneas de salida tienen perfecto sentido.

Seis segundos después de que se inicie el programa, se agota el tiempo de invocación del método y el resultado se devuelve como una lista de Futuros . Esto se puede ver en la cadena de salida Futuros recibidos .

Una vez realizadas las dos primeras tareas, comienzan dos más. Pero debido a que transcurre el tiempo de espera establecido en el método invokeAll , estas dos tareas no tienen tiempo para completarse. Reciben un comando de "cancelar" . Es por eso que la salida tiene dos líneas con el sueño 1: sueño interrumpido .

Y luego puedes ver dos líneas más con done . Este es el efecto secundario que mencioné cuando describí el método invocarTodo .

La quinta y última tarea ni siquiera se inicia, por lo que no vemos nada al respecto en la salida.

Las dos últimas líneas son el resultado de llamar a los métodos isShutdown e isTerpressed .

También es interesante ejecutar este ejemplo en modo de depuración y observar el estado de la tarea después de que transcurra el tiempo de espera (establezca un punto de interrupción en la línea con executorService.shutdown(); ):

Vemos que dos tareas se completaron normalmente y tres tareas se "Cancelaron" .

ScheduledExecutorService

Para concluir nuestra discusión sobre los ejecutores, echemos un vistazo a ScheduledExecutorService .

Tiene 4 métodos:

Método Descripción
programa público ScheduledFuture<?> (comando ejecutable, retraso largo, unidad TimeUnit); Programa la tarea Runnable pasada para que se ejecute una vez después del retraso especificado como argumento.
programación pública <V> ScheduledFuture<V> (llamable<V> invocable, demora larga, unidad de unidad de tiempo); Programa la tarea invocable pasada para que se ejecute una vez después del retraso especificado como argumento.
public ScheduledFuture<?> ScheduleAtFixedRate(comando ejecutable, retardo inicial largo, período largo, unidad de unidad de tiempo); Programa la ejecución periódica de la tarea pasada, que se ejecutará por primera vez después de initialDelay y cada ejecución posterior comenzará después de period .
public ScheduledFuture<?> scheduleWithFixedDelay(Comando ejecutable, retardo inicial largo, retardo largo, unidad TimeUnit); Programa la ejecución periódica de la tarea pasada, que se ejecutará por primera vez después de initialDelay , y cada ejecución posterior comenzará después del retraso (el período entre la finalización de la ejecución anterior y el inicio de la actual).