Eine Anleitung zum Java ExecutorService

Eine Anleitung zum Java ExecutorService

1. Überblick

ExecutorService ist ein vom JDK bereitgestelltes Framework, das die Ausführung von Aufgaben im asynchronen Modus vereinfacht. Im Allgemeinen stelltExecutorService automatisch einen Pool von Threads und APIs zur Verfügung, um ihm Aufgaben zuzuweisen.

Weitere Lektüre:

Leitfaden zum Fork / Join-Framework in Java

Eine Einführung in das in Java 7 vorgestellte Fork / Join-Framework und die Tools zur Beschleunigung der Parallelverarbeitung, indem versucht wird, alle verfügbaren Prozessorkerne zu verwenden.

Read more

Überblick über das java.util.concurrent

Entdecken Sie den Inhalt des Pakets java.util.concurrent.

Read more

Anleitung zu java.util.concurrent.Locks

In diesem Artikel werden verschiedene Implementierungen der Lock-Schnittstelle und der in Java 9 neu eingeführten StampedLock-Klasse erläutert.

Read more

2. Instanziieren vonExecutorService

2.1. Fabrikmethoden der KlasseExecutors

Der einfachste Weg,ExecutorService zu erstellen, ist die Verwendung einer der Factory-Methoden der KlasseExecutors.

Die folgende Codezeile erstellt beispielsweise einen Thread-Pool mit 10 Threads:

ExecutorService executor = Executors.newFixedThreadPool(10);

Es gibt verschiedene andere Factory-Methoden, um vordefinierteExecutorServicezu erstellen, die bestimmten Anwendungsfällen entsprechen. Um die beste Methode für Ihre Anforderungen zu finden, konsultieren SieOracle’s official documentation.

2.2. Erstellen Sie direkt einExecutorService

DaExecutorService eine Schnittstelle ist, kann eine Instanz einer ihrer Implementierungen verwendet werden. Im Paketjava.util.concurrenttehen mehrere Implementierungen zur Auswahl, oder Sie können Ihre eigenen erstellen.

Beispielsweise verfügt die KlasseThreadPoolExecutorüber einige Konstruktoren, mit denen ein Executor-Service und sein interner Pool konfiguriert werden können.

ExecutorService executorService =
  new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
  new LinkedBlockingQueue());

Möglicherweise stellen Sie fest, dass der obige Code densource code der werkseitigen MethodenewSingleThreadExecutor(). sehr ähnlich ist. In den meisten Fällen ist eine detaillierte manuelle Konfiguration nicht erforderlich.

3. Zuweisen von Aufgaben zuExecutorService

ExecutorService kannRunnable undCallable Aufgaben ausführen. Um die Dinge in diesem Artikel einfach zu halten, werden zwei grundlegende Aufgaben verwendet. Beachten Sie, dass hier Lambda-Ausdrücke anstelle anonymer innerer Klassen verwendet werden:

Runnable runnableTask = () -> {
    try {
        TimeUnit.MILLISECONDS.sleep(300);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Callable callableTask = () -> {
    TimeUnit.MILLISECONDS.sleep(300);
    return "Task's execution";
};

List> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);

Aufgaben können denExecutorService mit verschiedenen Methoden zugewiesen werden, einschließlichexecute(), die von derExecutor-Schnittstelle geerbt werden, undsubmit(),invokeAny(), invokeAll().

Die Methodeexecute()istvoid, und bietet keine Möglichkeit, das Ergebnis der Ausführung der Aufgabe abzurufen oder den Status der Aufgabe zu überprüfen (wird sie ausgeführt oder ausgeführt).

executorService.execute(runnableTask);

submit() sendet eineCallable- oder eineRunnable-Aufgabe an eineExecutorService und gibt ein Ergebnis vom TypFuture zurück.

Future future =
  executorService.submit(callableTask);

invokeAny() weistExecutorService, eine Sammlung von Aufgaben zu, wodurch jede ausgeführt wird, und gibt das Ergebnis einer erfolgreichen Ausführung einer Aufgabe zurück (wenn eine erfolgreiche Ausführung stattgefunden hat).

String result = executorService.invokeAny(callableTasks);

invokeAll() weist einemExecutorService, eine Sammlung von Aufgaben zu, wodurch jede ausgeführt wird, und gibt das Ergebnis aller Aufgabenausführungen in Form einer Liste von Objekten vom TypFuture. zurück

List> futures = executorService.invokeAll(callableTasks);

Bevor wir fortfahren, müssen zwei weitere Dinge besprochen werden: Herunterfahren vonExecutorService und Behandeln vonFuture Rückgabetypen.

4. ExecutorService herunterfahren

Im Allgemeinen werden dieExecutorService nicht automatisch zerstört, wenn keine Aufgabe zu verarbeiten ist. Es wird am Leben bleiben und auf neue Arbeiten warten.

In einigen Fällen ist dies sehr hilfreich. Zum Beispiel, wenn eine App Aufgaben verarbeiten muss, die unregelmäßig angezeigt werden, oder die Menge dieser Aufgaben zum Zeitpunkt der Kompilierung nicht bekannt ist.

Auf der anderen Seite könnte eine App ihr Ende erreichen, aber sie wird nicht gestoppt, da ein Warten vonExecutorService dazu führt, dass die JVM weiter ausgeführt wird.

UmExecutorService ordnungsgemäß herunterzufahren, haben wir die APIsshutdown() undshutdownNow().

Dieshutdown()-Methode führt nicht zu einer sofortigen Zerstörung derExecutorService.. Dadurch werden dieExecutorService keine neuen Aufgaben mehr annehmen und herunterfahren, nachdem alle laufenden Threads ihre aktuelle Arbeit beendet haben.

executorService.shutdown();

Die MethodeshutdownNow() versucht, dieExecutorService sofort zu zerstören, garantiert jedoch nicht, dass alle laufenden Threads gleichzeitig gestoppt werden. Diese Methode gibt eine Liste der Aufgaben zurück, die auf ihre Bearbeitung warten. Es ist Sache des Entwicklers, zu entscheiden, was mit diesen Aufgaben zu tun ist.

List notExecutedTasks = executorService.shutDownNow();

Eine gute Möglichkeit,ExecutorService (das auchrecommended by Oracle ist) herunterzufahren, besteht darin, beide Methoden in Kombination mit der MethodeawaitTermination() zu verwenden. Bei diesem Ansatz nehmen dieExecutorService zunächst keine neuen Aufgaben mehr an und warten bis zu einem bestimmten Zeitraum, bis alle Aufgaben abgeschlossen sind. Wenn diese Zeit abgelaufen ist, wird die Ausführung sofort gestoppt:

executorService.shutdown();
try {
    if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
        executorService.shutdownNow();
    }
} catch (InterruptedException e) {
    executorService.shutdownNow();
}

5. DieFuture-Schnittstelle

Die Methodensubmit() undinvokeAll() geben ein Objekt oder eine Sammlung von Objekten vom TypFuture zurück, wodurch wir das Ergebnis der Ausführung einer Aufgabe abrufen oder den Status der Aufgabe überprüfen können (wird sie ausgeführt) oder ausgeführt).

DieFuture-Schnittstelle bietet eine spezielle Blockierungsmethodeget(), die ein tatsächliches Ergebnis der Ausführung der AufgabeCallableodernull im Fall der AufgabeRunnablezurückgibt. Wenn Sie die Methodeget() aufrufen, während die Aufgabe noch ausgeführt wird, wird die Ausführung blockiert, bis die Aufgabe ordnungsgemäß ausgeführt wird und das Ergebnis verfügbar ist.

Future future = executorService.submit(callableTask);
String result = null;
try {
    result = future.get();
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

Bei einer sehr langen Blockierung, die durch dieget()-Methode verursacht wird, kann sich die Leistung einer Anwendung verschlechtern. Wenn die resultierenden Daten nicht entscheidend sind, können Sie ein solches Problem mithilfe von Zeitüberschreitungen vermeiden:

String result = future.get(200, TimeUnit.MILLISECONDS);

Wenn die Ausführungsdauer länger als angegeben ist (in diesem Fall 200 Millisekunden), wird einTimeoutException ausgelöst.

Mit der MethodeisDone() kann überprüft werden, ob die zugewiesene Aufgabe bereits verarbeitet wurde oder nicht.

DieFuture-Schnittstelle ermöglicht auch das Abbrechen der Aufgabenausführung mit dercancel()-Methode und das Überprüfen der Löschung mit derisCancelled()-Methode:

boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();

6. DieScheduledExecutorService-Schnittstelle

DasScheduledExecutorService führt Aufgaben nach einer vordefinierten Verzögerung und / oder in regelmäßigen Abständen aus. Der beste Weg, um einScheduledExecutorService zu instanziieren, ist die Verwendung der Factory-Methoden derExecutors-Klasse.

Für diesen Abschnitt wird einScheduledExecutorService mit einem Thread verwendet:

ScheduledExecutorService executorService = Executors
  .newSingleThreadScheduledExecutor();

Um die Ausführung einer einzelnen Aufgabe nach einer festgelegten Verzögerung zu planen, verwenden Sie diescheduled()-Methode derScheduledExecutorService. Es gibt zweischeduled() Methoden, mit denen SieRunnable oderCallable Aufgaben ausführen können:

Future resultFuture =
  executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

Mit der MethodescheduleAtFixedRate() kann eine Aufgabe nach einer festen Verzögerung regelmäßig ausgeführt werden. Der obige Code verzögert sich um eine Sekunde, bevorcallableTask ausgeführt wird.

Der folgende Codeblock führt eine Task nach einer anfänglichen Verzögerung von 100 Millisekunden aus und danach alle 450 Millisekunden dieselbe Task. Wenn der Prozessor mehr Zeit benötigt, um eine zugewiesene Aufgabe auszuführen als der Parameterperiod der MethodescheduleAtFixedRate(), warten dieScheduledExecutorService, bis die aktuelle Aufgabe abgeschlossen ist, bevor sie mit der nächsten beginnen:

Future resultFuture = service
  .scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

Wenn zwischen den Iterationen der Aufgabe eine feste Längenverzögerung erforderlich ist, solltescheduleWithFixedDelay() verwendet werden. Der folgende Code garantiert beispielsweise eine Pause von 150 Millisekunden zwischen dem Ende der aktuellen Ausführung und dem Beginn einer anderen Ausführung.

service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);

Gemäß den MethodenverträgenscheduleAtFixedRate() undscheduleWithFixedDelay() endet die Periodenausführung der Task mit der Beendigung derExecutorService oder wenn während der Taskausführung. eine Ausnahme ausgelöst wird

7. ExecutorService vs. Fork/Join

Nach der Veröffentlichung von Java 7 entschieden viele Entwickler, dass dasExecutorService-Framework durch das Fork / Join-Framework ersetzt werden sollte. Dies ist jedoch nicht immer die richtige Entscheidung. Trotz der Einfachheit der Verwendung und der häufigen Leistungsverbesserungen, die mit dem Verzweigen / Verbinden einhergehen, verringert sich auch die Kontrolle des Entwicklers über die gleichzeitige Ausführung.

ExecutorService gibt dem Entwickler die Möglichkeit, die Anzahl der generierten Threads und die Granularität der Aufgaben zu steuern, die von separaten Threads ausgeführt werden sollen. Der beste Anwendungsfall fürExecutorService ist die Verarbeitung unabhängiger Aufgaben, wie z. B. Transaktionen oder Anforderungen, gemäß dem Schema „Ein Thread für eine Aufgabe“.

Im Gegensatz dazu wurdeaccording to Oracle’s documentation, Gabel / Verbindung entwickelt, um die Arbeit zu beschleunigen, die rekursiv in kleinere Teile zerlegt werden kann.

8. Fazit

Trotz der relativen Einfachheit vonExecutorService gibt es einige häufige Fallstricke. Fassen wir sie zusammen:

Keeping an unused ExecutorService alive: In Abschnitt 4 dieses Artikels finden Sie eine ausführliche Erläuterung zum Herunterfahren vonExecutorService.

Wrong thread-pool capacity while using fixed length thread-pool: Es ist sehr wichtig zu bestimmen, wie viele Threads die Anwendung benötigt, um Aufgaben effizient auszuführen. Ein zu großer Thread-Pool verursacht unnötigen Overhead, nur um Threads zu erstellen, die sich meist im Wartemodus befinden. Zu wenige können dazu führen, dass eine Anwendung aufgrund langer Wartezeiten für Aufgaben in der Warteschlange nicht mehr reagiert.

Calling a Future‘s get() method after task cancellation: Der Versuch, das Ergebnis einer bereits abgebrochenen Aufgabe zu erhalten, löstCancellationException. aus

Unexpectedly-long blocking with Future‘s get() method: Timeouts sollten verwendet werden, um unerwartete Wartezeiten zu vermeiden.

Der Code für diesen Artikel ist ina GitHub repository verfügbar.