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:
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 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 Executors
newFixedThreadPool()
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 ExecutorService
execute(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 ExecutorService
submit(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 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.
Il metodo submit()
restituisce un oggetto Java Future che può essere usato per controllare quando il Runnable
ha completato.
Submit Callable
Il metodo Java ExecutorService
submit(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 ExecutorService
Callable
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 Runnable
run()
e il metodo Callable
call()
è 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 ExecutorService
shutdownNow
:
executorService.shutdownNow();
awaitTermination()
Il metodo ExecutorService
awaitTermination()
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 ExecutorService
awaitTermination()
:
executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );