De Java ExecutorService interface, java.util.concurrent.ExecutorService
, vertegenwoordigt een asynchroon uitvoeringsmechanisme dat in staat is om taken gelijktijdig op de achtergrond uit te voeren. In deze Java ExecutorService
tutorial leg ik uit hoe je een ExecutorService
maakt, hoe je er taken voor uitvoering aan voorlegt, hoe je de resultaten van die taken ziet, en hoe je de ExecutorService
weer afsluit als je dat nodig hebt.
Java ExecutorService Video Tutorial
Als u de voorkeur geeft aan video, dan heb ik hier een video introductie:
Taak Delegatie
Hier ziet u een diagram dat een thread illustreert die een taak delegeert aan een Java ExecutorService
voor asynchrone uitvoering:
Een thread die een taak delegeert aan een ExecutorService voor asynchrone uitvoering.
Als de thread de taak eenmaal heeft gedelegeerd aan de ExecutorService
, gaat de thread verder met zijn eigen uitvoering, onafhankelijk van de uitvoering van die taak. De ExecutorService
voert de taak dan gelijktijdig uit, onafhankelijk van de thread die de taak heeft ingediend.
Java ExecutorService Voorbeeld
Voordat we te diep ingaan op de ExecutorService
, laten we eerst naar een eenvoudig voorbeeld kijken. Hier is een eenvoudig Java ExecutorService
voorbeeld:
ExecutorService executorService = Executors.newFixedThreadPool(10);executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();
Eerst wordt een ExecutorService
aangemaakt met behulp van de Executors
newFixedThreadPool()
factory method. Hierdoor ontstaat een thread pool met 10 threads die taken uitvoeren.
Ten tweede wordt een anonieme implementatie van de Runnable
interface doorgegeven aan de execute()
methode. Dit zorgt ervoor dat de Runnable
wordt uitgevoerd door een van de threads in de ExecutorService
.
U zult in deze tutorial nog verschillende voorbeelden zien van het gebruik van de ExecutorService
. Dit voorbeeld dient enkel om u een snel overzicht te geven van hoe het gebruik van een ExecutorService
om taken op de achtergrond uit te voeren eruit ziet.
Java ExecutorService Implementations
De Java ExecutorService
is zeer vergelijkbaar met een thread pool. In feite is de implementatie van de ExecutorService
interface die in het java.util.concurrent
pakket zit een thread pool implementatie. Als u wilt begrijpen hoe de ExecutorService
interface intern kan worden geïmplementeerd, lees dan de bovenstaande tutorial.
Aangezien ExecutorService
een interface is, hebt u de implementaties ervan nodig om er enig gebruik van te kunnen maken. De ExecutorService
heeft de volgende implementatie in het java.util.concurrent
pakket:
- ThreadPoolExecutor
- ScheduledThreadPoolExecutor
Een ExecutorService maken
Hoe u een ExecutorService
maakt, hangt af van de implementatie die u gebruikt. U kunt echter de Executors
factory class gebruiken om ook ExecutorService
instanties te maken. Hier zijn een paar voorbeelden van het maken van een ExecutorService
:
ExecutorService executorService1 = Executors.newSingleThreadExecutor();ExecutorService executorService2 = Executors.newFixedThreadPool(10);ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
ExecutorService Usage
Er zijn een paar verschillende manieren om taken voor uitvoering te delegeren aan een ExecutorService
:
- uitvoeren(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(…)
- invokeAll(…)
Ik zal elk van deze methoden in de volgende secties bekijken.
Uitvoeren Runnable
De Java ExecutorService
execute(Runnable)
methode neemt een java.lang.Runnable
object, en voert het asynchroon uit. Hier is een voorbeeld van het uitvoeren van een Runnable
met een ExecutorService
:
ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();
Er is geen manier om het resultaat van de uitgevoerde Runnable
te verkrijgen, indien nodig. U zult daarvoor een Callable
moeten gebruiken (uitgelegd in de volgende secties).
Submit Runnable
De Java ExecutorService
submit(Runnable)
methode neemt ook een Runnable
implementatie, maar retourneert een Future
object. Dit Future
object kan worden gebruikt om te controleren of de Runnable
klaar is met uitvoeren.
Hier volgt een Java ExecutorService
submit()
voorbeeld:
Future future = executorService.submit(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});future.get(); //returns null if the task has finished correctly.
De methode submit()
retourneert een Java Future-object dat kan worden gebruikt om te controleren wanneer de Runnable
is voltooid.
Submit Callable
De Java ExecutorService
submit(Callable)
methode is vergelijkbaar met de submit(Runnable)
methode behalve dat het een Java Callable neemt in plaats van een Runnable
. Het precieze verschil tussen een Callable
en een Runnable
wordt verderop uitgelegd.
Het resultaat van de Callable
kan worden verkregen via het Java Future object dat door de submit(Callable)
methode wordt geretourneerd. Hier is een ExecutorService
Callable
voorbeeld:
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());
Het bovenstaande code-voorbeeld zal het volgende opleveren:
Asynchronous Callablefuture.get() = Callable Result
invokeAny()
De methode invokeAny()
neemt een verzameling Callable
-objecten, of subinterfaces van Callable
. Het aanroepen van deze methode geeft geen Future
terug, maar geeft het resultaat van een van de Callable
objecten terug. U hebt geen garantie over welk van de Callable
’s resultaten u krijgt. Gewoon een van degenen die afmaken.
Als een van de Callable’s afmaakt, zodat een resultaat wordt geretourneerd van invokeAny()
, dan wordt de rest van de Callable instanties geannuleerd.
Als een van de taken afmaakt (of een exception gooit), dan wordt de rest van de Callable
’s geannuleerd.
Hier is een code voorbeeld:
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();
Dit code voorbeeld drukt het object af dat door een van de Callable
’s in de gegeven collectie wordt teruggegeven. Ik heb geprobeerd het een paar keer uit te voeren, en het resultaat verandert. Soms is het “Taak 1”, soms “Taak 2” etc.
invokeAll()
De methode invokeAll()
roept alle Callable
-objecten op die je aan hem doorgeeft in de verzameling die als parameter is doorgegeven. De invokeAll()
retourneert een lijst van Future
objecten via welke u de resultaten van de uitvoeringen van elke Callable
kunt verkrijgen.
Houd in gedachten dat een taak kan eindigen als gevolg van een uitzondering, dus het kan zijn dat hij niet “geslaagd” is. Er is geen manier op een Future
om het verschil te zien.
Hier is een code voorbeeld:
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
De Runnable
interface lijkt erg op de Callable
interface. De Runnable interface vertegenwoordigt een taak die gelijktijdig kan worden uitgevoerd door een thread of een ExecutorService
. De Callable kan alleen worden uitgevoerd door een ExecutorService. Beide interfaces hebben slechts een enkele methode. Er is echter een klein verschil tussen de Callable
en Runnable
interface. Het verschil tussen de Runnable
en Callable
interface is beter zichtbaar als je de interface declaraties ziet.
Hier is eerst de Runnable
interface declaratie:
public interface Runnable { public void run();}
En hier is de Callable
interface declaratie:
public interface Callable{ public Object call() throws Exception;}
Het belangrijkste verschil tussen de Runnable
run()
methode en de Callable
call()
methode is dat de call()
methode een Object
kan teruggeven bij het aanroepen van de methode. Een ander verschil tussen call()
en run()
is dat call()
een exceptie kan werpen, terwijl run()
dat niet kan (behalve voor niet-gecontroleerde excepties – subklassen van RuntimeException
).
Als u een taak aan een Java ExecutorService
moet voorleggen en u hebt een resultaat van de taak nodig, dan moet u uw taak de Callable
interface laten implementeren. Anders kan uw taak gewoon de Runnable
interface implementeren.
Taak annuleren
U kunt een taak (Runnable
of Callable
) annuleren die is ingediend bij een Java ExecutorService
door de cancel()
methode op te roepen op de Future
die wordt geretourneerd wanneer de taak is ingediend. Het annuleren van de taak is alleen mogelijk als de taak nog niet begonnen is met uitvoeren. Hier is een voorbeeld van het annuleren van een taak door de Future.cancel()
methode aan te roepen:
future.cancel();
ExecutorService Shutdown
Als u klaar bent met het gebruik van de Java ExecutorService
moet u deze afsluiten, zodat de threads niet blijven lopen. Als uw toepassing wordt gestart via een main()
methode en uw hoofddraad verlaat uw toepassing, dan zal de toepassing blijven lopen als u een actieve ExexutorService
in uw toepassing heeft. De actieve threads binnen deze ExecutorService
voorkomen dat de JVM wordt afgesloten.
shutdown()
Om de threads binnen de ExecutorService
te beëindigen, roept u de shutdown()
methode ervan op. De ExecutorService
zal niet onmiddellijk sluiten, maar het zal niet langer nieuwe taken accepteren, en zodra alle threads klaar zijn met de huidige taken, sluit de ExecutorService
zich af. Alle taken die aan de ExecutorService
zijn voorgelegd voordat shutdown()
wordt aangeroepen, worden uitgevoerd. Hier is een voorbeeld van het uitvoeren van een Java ExecutorService
shutdown:
executorService.shutdown();
shutdownNow()
Als u de ExecutorService
onmiddellijk wilt afsluiten, kunt u de shutdownNow()
methode aanroepen. Dit zal proberen om alle uitvoerende taken meteen te stoppen, en slaat alle ingediende maar niet verwerkte taken over. Er worden geen garanties gegeven over de uitvoerende taken. Misschien stoppen ze, misschien gaan ze door tot het einde. Het is een poging naar best vermogen. Hier is een voorbeeld van het aanroepen van ExecutorService
shutdownNow
:
executorService.shutdownNow();
awaitTermination()
De ExecutorService
awaitTermination()
methode zal de thread die het aanroept blokkeren totdat ofwel de ExecutorService
zich volledig heeft afgesloten, of tot een bepaalde time out optreedt. De awaitTermination()
methode wordt typisch aangeroepen na het aanroepen van shutdown()
of shutdownNow()
. Hier is een voorbeeld van het aanroepen van ExecutorService
awaitTermination()
:
executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );