L’interfaccia Java ExecutorService, java.util.concurrent.ExecutorService, rappresenta un meccanismo di esecuzione asincrona che è in grado di eseguire compiti simultaneamente in background. In questo tutorial Java ExecutorService spiegherò come creare un ExecutorService, come sottoporgli dei compiti da eseguire, come vedere i risultati di questi compiti, e come chiudere nuovamente il ExecutorService quando è necessario.

Java ExecutorService Video Tutorial

Se preferite il video, ho un video di introduzione al qui:

Video Tutorial Java ExecutorService - Parte 1
Video Tutorial Java ExecutorService - Parte 2

Delega dei task

Ecco un diagramma che illustra un thread che delega un task a un Java ExecutorService per l’esecuzione asincrona:

Un thread che delega un compito ad un ExecutorService per l'esecuzione asincrona.

Un thread che delega un compito a un ExecutorService per l’esecuzione asincrona.

Una volta che il thread ha delegato il compito al ExecutorService, il thread continua la propria esecuzione indipendentemente dall’esecuzione di quel compito. Il ExecutorService esegue quindi il compito simultaneamente, indipendentemente dal thread che ha presentato il compito.

Java ExecutorService Example

Prima di addentrarci troppo nel ExecutorService, guardiamo un semplice esempio. Ecco un semplice esempio Java ExecutorService:

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

Prima viene creato un ExecutorService usando il ExecutorsnewFixedThreadPool() metodo factory. Questo crea un pool di thread con 10 thread che eseguono compiti.

In secondo luogo, un’implementazione anonima dell’interfaccia Runnable viene passata al metodo execute(). Questo fa sì che il Runnable venga eseguito da uno dei thread nel ExecutorService.

Vedrete molti altri esempi di come usare il ExecutorService in questo tutorial. Questo esempio è servito solo per darvi una rapida panoramica di come appare l’uso di un ExecutorService per eseguire compiti in background.

Implementazioni Java ExecutorService

Il Java ExecutorService è molto simile a un pool di thread. Infatti, l’implementazione dell’interfaccia ExecutorService presente nel pacchetto java.util.concurrent è un’implementazione di thread pool. Se volete capire come l’interfaccia ExecutorService può essere implementata internamente, leggete il tutorial di cui sopra.

Poiché ExecutorService è un’interfaccia, avete bisogno delle sue implementazioni per poterne fare qualsiasi uso. Il ExecutorService ha la seguente implementazione nel pacchetto java.util.concurrent:

  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

Creazione di un ExecutorService

Come si crea un ExecutorService dipende dall’implementazione utilizzata. Tuttavia, è possibile utilizzare la classe Executors factory per creare istanze ExecutorService. Ecco alcuni esempi di creazione di un ExecutorService:

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

Uso di ExecutorService

Ci sono alcuni modi diversi per delegare compiti da eseguire a un ExecutorService:

  • execute(Runnable)
  • submit(Runnable)
  • submit(Callable)
  • invokeAny(…)
  • invokeAll(…)

Darò uno sguardo a ciascuno di questi metodi nelle sezioni seguenti.

Execute Runnable

Il metodo Java ExecutorServiceexecute(Runnable) prende un oggetto java.lang.Runnable e lo esegue in modo asincrono. Ecco un esempio di esecuzione di un Runnable con un ExecutorService:

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

Non c’è modo di ottenere il risultato del Runnable eseguito, se necessario. Dovrete usare un Callable per questo (spiegato nelle sezioni seguenti).

Submit Runnable

Il metodo Java ExecutorServicesubmit(Runnable) prende anche un’implementazione Runnable, ma ritorna un oggetto Future. Questo oggetto Future può essere usato per controllare se il Runnable ha finito l’esecuzione.

Ecco un esempio 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.

Il metodo submit() restituisce un oggetto Java Future che può essere usato per controllare quando il Runnable ha completato.

Submit Callable

Il metodo Java ExecutorServicesubmit(Callable) è simile al metodo submit(Runnable) tranne che prende un Java Callable invece di un Runnable. La differenza precisa tra un Callable e un Runnable è spiegata più avanti.

Il risultato del Callable può essere ottenuto tramite l’oggetto Java Future restituito dal metodo submit(Callable). Ecco un ExecutorServiceCallable esempio:

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());

L’esempio di codice precedente produrrà questo risultato:

Asynchronous Callablefuture.get() = Callable Result

invokeAny()

Il metodo invokeAny() prende una collezione di oggetti Callable, o sottointerfacce di Callable. Invocare questo metodo non restituisce un Future, ma restituisce il risultato di uno degli oggetti Callable. Non si ha alcuna garanzia su quale dei risultati di Callable si ottiene. Solo uno di quelli che finiscono.

Se un Callable finisce, in modo che un risultato venga restituito da invokeAny(), allora il resto delle istanze del Callable viene cancellato.

Se uno dei compiti viene completato (o lancia un’eccezione), il resto dei Callable viene cancellato.

Ecco un esempio di codice:

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();

Questo esempio di codice stamperà l’oggetto restituito da uno dei Callable nella collezione data. Ho provato a eseguirlo alcune volte e il risultato cambia. A volte è “Task 1”, a volte “Task 2” ecc.

invokeAll()

Il metodo invokeAll() invoca tutti gli oggetti Callable che gli passi nella collezione passata come parametro. Il invokeAll() restituisce una lista di oggetti Future tramite la quale è possibile ottenere i risultati delle esecuzioni di ogni Callable.

Tenete presente che un compito potrebbe terminare a causa di un’eccezione, quindi potrebbe non essere “riuscito”. Non c’è modo su un Future di capire la differenza.

Ecco un esempio di codice:

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();

Runnable vs. Callable

L’interfaccia Runnable è molto simile all’interfaccia Callable. L’interfaccia Runnable rappresenta un compito che può essere eseguito contemporaneamente da un thread o da un ExecutorService. Il Callable può essere eseguito solo da un ExecutorService. Entrambe le interfacce hanno un solo metodo. C’è una piccola differenza tra l’interfaccia Callable e Runnable. La differenza tra l’interfaccia Runnable e Callable è più facilmente visibile quando si vedono le dichiarazioni di interfaccia.

Ecco la prima dichiarazione di interfaccia Runnable:

public interface Runnable { public void run();}

Ed ecco la dichiarazione di interfaccia Callable:

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

La differenza principale tra il metodo Runnablerun() e il metodo Callablecall() è che il metodo call() può restituire un Object dalla chiamata del metodo. Un’altra differenza tra call() e run() è che call() può lanciare un’eccezione, mentre run() non può (tranne per le eccezioni non controllate – sottoclassi di RuntimeException).

Se avete bisogno di inviare un compito a un Java ExecutorService e avete bisogno di un risultato dal compito, allora dovete far sì che il vostro compito implementi l’interfaccia Callable. Altrimenti il vostro task può semplicemente implementare l’interfaccia Runnable.

Annulla compito

È possibile annullare un compito (Runnable o Callable) sottoposto ad un Java ExecutorService chiamando il metodo cancel() sul Future restituito quando il compito viene inviato. L’annullamento del compito è possibile solo se il compito non ha ancora iniziato l’esecuzione. Ecco un esempio di cancellazione di un task chiamando il metodo Future.cancel():

future.cancel();

ExecutorService Shutdown

Quando hai finito di usare il Java ExecutorService dovresti chiuderlo, così i thread non continuano a girare. Se la vostra applicazione viene avviata tramite un metodo main() e il vostro thread principale esce dalla vostra applicazione, l’applicazione continuerà a funzionare se avete un ExexutorService attivo nella vostra applicazione. I thread attivi all’interno di questo ExecutorService impediscono alla JVM di spegnersi.

shutdown()

Per terminare i thread all’interno del ExecutorService si chiama il suo metodo shutdown(). Il ExecutorService non si spegne immediatamente, ma non accetta più nuovi compiti, e una volta che tutti i thread hanno finito i compiti in corso, il ExecutorService si spegne. Tutti i compiti inviati al ExecutorService prima che shutdown() venga chiamato, vengono eseguiti. Ecco un esempio di esecuzione di un arresto Java ExecutorService:

executorService.shutdown();

shutdownNow()

Se volete spegnere il ExecutorService immediatamente, potete chiamare il metodo shutdownNow(). Questo tenterà di fermare subito tutti i compiti in esecuzione, e salta tutti i compiti inviati ma non processati. Non ci sono garanzie riguardo ai compiti in esecuzione. Forse si fermano, forse vengono eseguiti fino alla fine. È un tentativo al meglio. Ecco un esempio di chiamata ExecutorServiceshutdownNow:

executorService.shutdownNow();

awaitTermination()

Il metodo ExecutorServiceawaitTermination() bloccherà il thread che lo chiama finché il ExecutorService non si sarà spento completamente, o finché non si verifica un determinato time out. Il metodo awaitTermination() è tipicamente chiamato dopo aver chiamato shutdown() o shutdownNow(). Ecco un esempio di chiamata ExecutorServiceawaitTermination():

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

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *