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:

Java ExecutorService Tutorial Video - Teil 1
Java ExecutorService Tutorial Video - Teil 2

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 zur asynchronen Ausführung an einen ExecutorService 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 ExecutorsnewFixedThreadPool()-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 ExecutorServiceexecute(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 ExecutorServicesubmit(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 ExecutorServicesubmit() 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 ExecutorServicesubmit(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 ExecutorServiceCallable 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 Runnablerun() Methode und der Callablecall() 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 ExecutorServiceshutdownNow:

executorService.shutdownNow();

awaitTermination()

Die ExecutorServiceawaitTermination()-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 ExecutorServiceawaitTermination():

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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.