Perché hai bisogno dell'interfaccia Executor?

Prima di Java 5, dovevi scrivere tutta la tua gestione dei thread di codice nella tua applicazione. Inoltre, creando unnuovo Filoobject è un'operazione che richiede molte risorse e non ha senso creare un nuovo thread per ogni attività leggera. E poiché questo problema è familiare a tutti gli sviluppatori di applicazioni multi-thread, hanno deciso di portare questa funzionalità in Java come framework Executor .

Qual è la grande idea? È semplice: invece di creare un nuovo thread per ogni nuova attività, i thread vengono conservati in una sorta di "archivio" e quando arriva una nuova attività, recuperiamo un thread esistente invece di crearne uno nuovo.

Le interfacce principali di questo framework sono Executor , ExecutorService e ScheduledExecutorService , ognuna delle quali estende le funzionalità della precedente.

L'interfaccia Executor è l'interfaccia di base. Dichiara un singolo metodo void execute (comando Runnable) implementato da un oggetto Runnable .

L' interfaccia ExecutorService è più interessante. Ha metodi per gestire il completamento del lavoro, nonché metodi per restituire un qualche tipo di risultato. Diamo un'occhiata più da vicino ai suoi metodi:

Metodo Descrizione
void spegnimento(); La chiamata a questo metodo interrompe ExecutorService . Tutte le attività che sono già state inviate per l'elaborazione verranno completate, ma non verranno accettate nuove attività.
List<Eseguibile> shutdownNow();

La chiamata a questo metodo interrompe ExecutorService . Thread.interrupt verrà chiamato per tutte le attività che sono già state inviate per l'elaborazione. Questo metodo restituisce un elenco di attività in coda.

Il metodo non attende il completamento di tutte le attività "in corso" al momento della chiamata al metodo.

Avviso: la chiamata a questo metodo potrebbe causare perdite di risorse.

booleano isShutdown(); Controlla se ExecutorService è stato arrestato.
booleano isTerminated(); Restituisce true se tutte le attività sono state completate dopo l'arresto di ExecutorService . Finché non viene chiamato shutdown() o shutdownNow() , restituirà sempre false .
boolean waitTermination(long timeout, TimeUnit unit) genera InterruptedException;

Dopo che il metodo shutdown() è stato chiamato, questo metodo blocca il thread su cui è chiamato, fino a quando non si verifica una delle seguenti condizioni:

  • tutte le attività pianificate sono state completate;
  • il timeout passato al metodo è scaduto;
  • il thread corrente viene interrotto.

Restituisce true se tutte le attività sono state completate e false se il timeout scade prima del termine.

<T> Future<T> submit(Callable<T> attività);

Aggiunge un'attività Callable a ExecutorService e restituisce un oggetto che implementa l' interfaccia Future .

<T> è il tipo del risultato dell'attività passata.

<T> Future<T> submit(Attività eseguibile, risultato T);

Aggiunge un'attività Runnable a ExecutorService e restituisce un oggetto che implementa l' interfaccia Future .

Il parametro T result è ciò che viene restituito da una chiamata al metodo get() sul risultatoOggetto futuro.

Future<?> submit(Attività eseguibile);

Aggiunge un'attività Runnable a ExecutorService e restituisce un oggetto che implementa l' interfaccia Future .

Se chiamiamo il metodo get() sull'oggetto Future risultante , otteniamo null.

<T> List<Future<T>> invokeAll(Collection<? estende le attività Callable<T>>) genera InterruptedException;

Passa un elenco di attività Callable a ExecutorService . Restituisce una lista di Futures da cui possiamo ottenere il risultato del lavoro. Questo elenco viene restituito quando tutte le attività inviate sono state completate.

Se la raccolta di attività viene modificata mentre il metodo è in esecuzione, il risultato di questo metodo non è definito.

<T> List<Future<T>> invokeAll(Collection<? estende attività Callable<T>>, timeout lungo, unità TimeUnit) genera InterruptedException;

Passa un elenco di attività Callable a ExecutorService . Restituisce una lista di Futures da cui possiamo ottenere il risultato del lavoro. Questo elenco viene restituito quando tutte le attività passate vengono completate o dopo che il timeout passato al metodo è scaduto, a seconda di quale evento si verifica per primo.

Se il timeout scade, le attività non completate vengono annullate.

Nota: è possibile che un'attività annullata non smetta di funzionare (vedremo questo effetto collaterale nell'esempio).

Se la raccolta di attività viene modificata mentre il metodo è in esecuzione, il risultato di questo metodo non è definito.

<T> T invokeAny(Collection<? estende le attività Callable<T>>) genera InterruptedException, ExecutionException;

Passa un elenco di attività Callable a ExecutorService . Restituisce il risultato di una delle attività (se presenti) completate senza generare un'eccezione (se presente).

Se la raccolta di attività viene modificata mentre il metodo è in esecuzione, il risultato di questo metodo non è definito.

<T> T invokeAny(Collection<? estende attività Callable<T>>, timeout lungo, unità TimeUnit) genera InterruptedException, ExecutionException, TimeoutException;

Passa un elenco di attività Callable a ExecutorService . Restituisce il risultato di una delle attività (se presenti) completate senza generare un'eccezione prima della scadenza del timeout passato al metodo.

Se la raccolta di attività viene modificata mentre il metodo è in esecuzione, il risultato di questo metodo non è definito.

Diamo un'occhiata a un piccolo esempio di utilizzo di 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;
       }
   }
}

Produzione:

done
done
Futuri ricevuti
sleep 1: sleep interrotto
sleep 1: sleep interrotto
done
done
true
true

Ogni attività viene eseguita per 5 secondi. Abbiamo creato un pool per due thread, quindi le prime due righe di output hanno perfettamente senso.

Sei secondi dopo l'avvio del programma, il metodo invokeAll scade e il risultato viene restituito come un elenco di Futures . Questo può essere visto dalla stringa di output Futures received .

Dopo che le prime due attività sono state completate, ne iniziano altre due. Ma poiché il timeout impostato nel metodo invokeAll scade, queste due attività non hanno tempo per essere completate. Ricevono un comando "annulla" . Ecco perché l'output ha due righe con sleep 1: sleep interrotto .

E poi puoi vedere altre due righe con done . Questo è l'effetto collaterale che ho menzionato quando ho descritto il metodo invokeAll .

La quinta e ultima attività non viene nemmeno avviata, quindi non vediamo nulla al riguardo nell'output.

Le ultime due righe sono il risultato della chiamata ai metodi isShutdown e isTerminated .

È anche interessante eseguire questo esempio in modalità debug e osservare lo stato dell'attività una volta trascorso il timeout (impostare un punto di interruzione sulla riga con executorService.shutdown(); ):

Vediamo che due attività sono state completate normalmente e tre attività sono state "annullate" .

ScheduledExecutorService

Per concludere la nostra discussione sugli esecutori, diamo un'occhiata a ScheduledExecutorService .

Ha 4 metodi:

Metodo Descrizione
public ScheduledFuture<?> schedule (comando eseguibile, lungo ritardo, unità TimeUnit); Pianifica l' attività Runnable passata in modo che venga eseguita una volta dopo il ritardo specificato come argomento.
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit); Pianifica l' attività Callable passata in modo che venga eseguita una volta dopo il ritardo specificato come argomento.
public ScheduledFuture<?> scheduleAtFixedRate(comando eseguibile, ritardo iniziale lungo, periodo lungo, unità TimeUnit); Pianifica l'esecuzione periodica dell'attività passata, che verrà eseguita per la prima volta dopo initialDelay e ogni esecuzione successiva inizierà dopo period .
public ScheduledFuture<?> scheduleWithFixedDelay(Comando eseguibile, ritardo iniziale lungo, ritardo lungo, unità TimeUnit); Pianifica l'esecuzione periodica dell'attività passata, che verrà eseguita per la prima volta dopo initialDelay e ogni esecuzione successiva inizierà dopo il ritardo (il periodo tra il completamento dell'esecuzione precedente e l'inizio di quella corrente).