Die Schnittstelle Java ExecutorService, java.util.concurrent.ExecutorService
, stellt einen asynchronen Ausführungsmechanismus dar, der in der Lage ist, Aufgaben parallel im Hintergrund auszuführen. In diesem Java-ExecutorService
-Tutorial erkläre ich, wie man ein ExecutorService
erstellt, wie man ihm Aufgaben zur Ausführung übergibt, wie man die Ergebnisse dieser Aufgaben sieht und wie man das ExecutorService
wieder herunterfährt, wenn man es braucht.
Java ExecutorService Video Tutorial
Wenn Sie ein Video bevorzugen, habe ich hier eine Videoeinführung:
Aufgabendelegation
Hier ist ein Diagramm, das einen Thread zeigt, der eine Aufgabe an ein Java ExecutorService
zur asynchronen Ausführung delegiert:
Ein Thread, der eine Aufgabe an einen ExecutorService zur asynchronen Ausführung delegiert.
Wenn der Thread die Aufgabe an das ExecutorService
delegiert hat, setzt der Thread seine eigene Ausführung unabhängig von der Ausführung dieser Aufgabe fort. Das ExecutorService
führt die Aufgabe dann gleichzeitig aus, unabhängig von dem Thread, der die Aufgabe eingereicht hat.
Java ExecutorService Beispiel
Bevor wir zu tief in das ExecutorService
einsteigen, wollen wir uns ein einfaches Beispiel ansehen. Hier ist ein einfaches Java-ExecutorService
-Beispiel:
ExecutorService executorService = Executors.newFixedThreadPool(10);executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();
Zunächst wird mit der Executors
newFixedThreadPool()
-Factory-Methode ein ExecutorService
erstellt. Dadurch wird ein Thread-Pool mit 10 Threads erzeugt, die Aufgaben ausführen.
Zweitens wird eine anonyme Implementierung der Runnable
-Schnittstelle an die execute()
-Methode übergeben. Dadurch wird das Runnable
von einem der Threads im ExecutorService
ausgeführt.
Sie werden im Laufe dieses Tutorials noch mehrere Beispiele für die Verwendung des ExecutorService
sehen. Dieses Beispiel diente nur dazu, Ihnen einen schnellen Überblick zu geben, wie die Verwendung eines ExecutorService
zur Ausführung von Aufgaben im Hintergrund aussieht.
Java ExecutorService Implementierungen
Das Java ExecutorService
ist einem Thread-Pool sehr ähnlich. Tatsächlich ist die Implementierung der ExecutorService
-Schnittstelle, die im Paket java.util.concurrent
vorhanden ist, eine Thread-Pool-Implementierung. Wenn Sie verstehen wollen, wie die ExecutorService
-Schnittstelle intern implementiert werden kann, lesen Sie das obige Tutorial.
Da es sich bei ExecutorService
um eine Schnittstelle handelt, benötigen Sie deren Implementierungen, um sie überhaupt nutzen zu können. Das ExecutorService
hat die folgende Implementierung im Paket java.util.concurrent
:
- ThreadPoolExecutor
- ScheduledThreadPoolExecutor
Erstellen eines ExecutorService
Wie Sie ein ExecutorService
erstellen, hängt von der verwendeten Implementierung ab. Sie können jedoch auch die Executors
-Fabrikklasse verwenden, um ExecutorService
-Instanzen zu erstellen. Hier sind ein paar Beispiele für die Erstellung eines ExecutorService
:
ExecutorService executorService1 = Executors.newSingleThreadExecutor();ExecutorService executorService2 = Executors.newFixedThreadPool(10);ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
ExecutorService Verwendung
Es gibt ein paar verschiedene Möglichkeiten, Aufgaben zur Ausführung an ein ExecutorService
zu delegieren:
- execute(Runnable)
- submit(Runnable)
- submit(Callable)
- invokeAny(…)
- invokeAll(…)
In den folgenden Abschnitten werde ich auf jede dieser Methoden eingehen.
Runnable ausführen
Die Java ExecutorService
execute(Runnable)
-Methode nimmt ein java.lang.Runnable
-Objekt und führt es asynchron aus. Hier ein Beispiel für die Ausführung eines Runnable
mit einem ExecutorService
:
ExecutorService executorService = Executors.newSingleThreadExecutor();executorService.execute(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});executorService.shutdown();
Es gibt keine Möglichkeit, das Ergebnis des ausgeführten Runnable
zu erhalten, falls nötig. Sie müssen dafür ein Callable
verwenden (wird in den folgenden Abschnitten erklärt).
Submit Runnable
Die Java-Methode ExecutorService
submit(Runnable)
nimmt ebenfalls eine Runnable
-Implementierung, gibt aber ein Future
-Objekt zurück. Dieses Future
-Objekt kann verwendet werden, um zu prüfen, ob das Runnable
seine Ausführung beendet hat.
Hier ist ein Java ExecutorService
submit()
Beispiel:
Future future = executorService.submit(new Runnable() { public void run() { System.out.println("Asynchronous task"); }});future.get(); //returns null if the task has finished correctly.
Die Methode submit()
gibt ein Java-Future-Objekt zurück, mit dem überprüft werden kann, wann das Runnable
abgeschlossen ist.
Submit Callable
Die Java-Methode ExecutorService
submit(Callable)
ähnelt der submit(Runnable)
-Methode, außer dass sie ein Java Callable statt eines Runnable
annimmt. Der genaue Unterschied zwischen einem Callable
und einem Runnable
wird etwas später erklärt.
Das Ergebnis des Callable
kann über das Java-Future-Objekt erhalten werden, das von der Methode submit(Callable)
zurückgegeben wird. Hier ein ExecutorService
Callable
Beispiel:
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());
Das obige Codebeispiel gibt folgendes aus:
Asynchronous Callablefuture.get() = Callable Result
invokeAny()
Die Methode invokeAny()
nimmt eine Sammlung von Callable
-Objekten oder Subinterfaces von Callable
. Der Aufruf dieser Methode gibt kein Future
zurück, sondern liefert das Ergebnis eines der Callable
-Objekte. Sie haben keine Garantie, welches der Ergebnisse des Callable
Sie erhalten. Nur eines von denen, die fertig werden.
Wenn ein Callable fertig wird, so dass ein Ergebnis von invokeAny()
zurückgegeben wird, dann werden die restlichen Callable-Instanzen abgebrochen.
Wenn eine der Aufgaben fertig wird (oder eine Exception wirft), dann werden die restlichen Callable
’s abgebrochen.
Hier ein Codebeispiel:
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();
Dieses Codebeispiel gibt das Objekt aus, das von einem der Callable
’s in der gegebenen Sammlung zurückgegeben wird. Ich habe versucht, es ein paar Mal auszuführen, und das Ergebnis ändert sich. Manchmal ist es „Aufgabe 1“, manchmal „Aufgabe 2“ usw.
invokeAll()
Die invokeAll()
-Methode ruft alle Callable
-Objekte in der als Parameter übergebenen Sammlung auf. Das invokeAll()
gibt eine Liste von Future
-Objekten zurück, über die Sie die Ergebnisse der Ausführungen jedes Callable
erhalten können.
Berücksichtigen Sie, dass eine Aufgabe aufgrund einer Ausnahme beendet werden kann, so dass sie möglicherweise nicht „erfolgreich“ war. Es gibt keine Möglichkeit, auf einem Future
den Unterschied zu erkennen.
Hier ein Code-Beispiel:
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
Callable
Die Runnable
-Schnittstelle ist der Callable
-Schnittstelle sehr ähnlich. Die Runnable-Schnittstelle repräsentiert eine Aufgabe, die von einem Thread oder einem ExecutorService
gleichzeitig ausgeführt werden kann. Das Callable kann nur von einem ExecutorService ausgeführt werden. Beide Schnittstellen haben nur eine einzige Methode. Es gibt allerdings einen kleinen Unterschied zwischen der Callable
und Runnable
Schnittstelle. Der Unterschied zwischen der Runnable
und Callable
Schnittstelle ist leichter zu erkennen, wenn Sie die Schnittstellendeklarationen sehen.
Hier ist zunächst die Runnable
Schnittstellendeklaration:
public interface Runnable { public void run();}
Und hier ist die Callable
Schnittstellendeklaration:
public interface Callable{ public Object call() throws Exception;}
Der Hauptunterschied zwischen der Runnable
run()
Methode und der Callable
call()
Methode ist, dass die call()
Methode ein Object
aus dem Methodenaufruf zurückgeben kann. Ein weiterer Unterschied zwischen call()
und run()
ist, dass call()
eine Exception werfen kann, während run()
dies nicht kann (außer bei ungeprüften Exceptions – Unterklassen von RuntimeException
).
Wenn Sie eine Aufgabe an ein Java ExecutorService
übergeben wollen und ein Ergebnis der Aufgabe benötigen, dann müssen Sie Ihre Aufgabe die Callable
-Schnittstelle implementieren lassen. Ansonsten kann Ihre Aufgabe einfach die Runnable
-Schnittstelle implementieren.
Aufgabe abbrechen
Sie können eine Aufgabe (Runnable
oder Callable
) abbrechen, die an ein Java ExecutorService
durch Aufruf der cancel()
-Methode auf dem Future
, das beim Übermitteln der Aufgabe zurückgegeben wird. Das Abbrechen der Aufgabe ist nur möglich, wenn die Ausführung der Aufgabe noch nicht begonnen hat. Hier ein Beispiel für das Abbrechen einer Aufgabe durch Aufruf der Future.cancel()
-Methode:
future.cancel();
ExecutorService Shutdown
Wenn Sie mit dem Java ExecutorService
fertig sind, sollten Sie es herunterfahren, damit die Threads nicht weiterlaufen. Wenn Ihre Anwendung über eine main()
-Methode gestartet wird und Ihr Haupt-Thread Ihre Anwendung verlässt, läuft die Anwendung weiter, wenn Sie ein aktives ExexutorService
in Ihrer Anwendung haben. Die aktiven Threads innerhalb dieses ExecutorService
verhindern, dass die JVM herunterfährt.
shutdown()
Um die Threads innerhalb des ExecutorService
zu beenden, rufen Sie dessen shutdown()
Methode auf. Das ExecutorService
wird nicht sofort heruntergefahren, aber es nimmt keine neuen Aufgaben mehr an, und sobald alle Threads aktuelle Aufgaben beendet haben, wird das ExecutorService
heruntergefahren. Alle Tasks, die vor dem Aufruf von shutdown()
an das ExecutorService
übergeben wurden, werden ausgeführt. Hier ein Beispiel für die Ausführung eines Java ExecutorService
-Shutdowns:
executorService.shutdown();
shutdownNow()
Wenn Sie das ExecutorService
sofort herunterfahren wollen, können Sie die Methode shutdownNow()
aufrufen. Diese versucht, alle ausführenden Tasks sofort zu stoppen, und überspringt alle eingereichten, aber nicht bearbeiteten Tasks. Es werden keine Garantien für die ausgeführten Tasks gegeben. Vielleicht halten sie an, vielleicht werden sie bis zum Ende ausgeführt. Es ist ein bestmöglicher Versuch. Hier ist ein Beispiel für den Aufruf von ExecutorService
shutdownNow
:
executorService.shutdownNow();
awaitTermination()
Die ExecutorService
awaitTermination()
-Methode blockiert den aufrufenden Thread so lange, bis entweder der ExecutorService
komplett heruntergefahren ist, oder bis ein bestimmter Timeout eintritt. Die Methode awaitTermination()
wird normalerweise nach dem Aufruf von shutdown()
oder shutdownNow()
aufgerufen. Hier ist ein Beispiel für den Aufruf von ExecutorService
awaitTermination()
:
executorService.shutdown();executorService.awaitTermination(10_000L, TimeUnit.MILLISECONDS );