ExecutorService - Warten, bis Threads beendet sind

ExecutorService - Warten auf das Beenden der Threads

1. Überblick

DasExecutorService-Framework erleichtert die Verarbeitung von Aufgaben in mehreren Threads. Wir werden einige Szenarien veranschaulichen, in denen wir darauf warten, dass Threads ihre Ausführung beenden.

Außerdem zeigen wir, wie Sie einExecutorService ordnungsgemäß herunterfahren und warten, bis bereits ausgeführte Threads ihre Ausführung abgeschlossen haben.

2. NachExecutor’s Herunterfahren

Wenn SieExecutor, verwenden, können Sie es herunterfahren, indem Sie die Methodenshutdown() odershutdownNow() aufrufen. Although, it won’t wait until all threads stop executing.

Das Warten, bis vorhandene Threads ihre Ausführung abgeschlossen haben, kann mithilfe derawaitTermination()-S-Methode erreicht werden.

Dies blockiert den Thread, bis alle Aufgaben ihre Ausführung abgeschlossen haben oder das angegebene Zeitlimit erreicht ist:

public void awaitTerminationAfterShutdown(ExecutorService threadPool) {
    threadPool.shutdown();
    try {
        if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
            threadPool.shutdownNow();
        }
    } catch (InterruptedException ex) {
        threadPool.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

3. Verwenden vonCountDownLatch

Schauen wir uns als nächstes einen anderen Ansatz zur Lösung dieses Problems an: Verwenden SieCountDownLatch, um den Abschluss einer Aufgabe zu signalisieren.

Wir können es mit einem Wert initialisieren, der angibt, wie oft es dekrementiert werden kann, bevor alle Threads benachrichtigt werden, die die Methodeawait()aufgerufen haben.

Wenn der aktuelle Thread beispielsweise darauf warten muss, dass weitereN-Threads ihre Ausführung beenden, können wir den Latch mitN initialisieren:

ExecutorService WORKER_THREAD_POOL
  = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
    WORKER_THREAD_POOL.submit(() -> {
        try {
            // ...
            latch.countDown();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    });
}

// wait for the latch to be decremented by the two remaining threads
latch.await();

4. Verwenden voninvokeAll()

Der erste Ansatz, mit dem wir Threads ausführen können, ist dieinvokeAll()-Methode. The method returns a list of Future objects after all tasks finish or the timeout expires.

Außerdem müssen wir beachten, dass die Reihenfolge der zurückgegebenenFuture-Objekte mit der Liste der bereitgestelltenCallable-Objekte übereinstimmt:

ExecutorService WORKER_THREAD_POOL = Executors.newFixedThreadPool(10);

List> callables = Arrays.asList(
  new DelayedCallable("fast thread", 100),
  new DelayedCallable("slow thread", 3000));

long startProcessingTime = System.currentTimeMillis();
List> futures = WORKER_THREAD_POOL.invokeAll(callables);

awaitTerminationAfterShutdown(WORKER_THREAD_POOL);

long totalProcessingTime = System.currentTimeMillis() - startProcessingTime;

assertTrue(totalProcessingTime >= 3000);

String firstThreadResponse = futures.get(0).get();

assertTrue("fast thread".equals(firstThreadResponse));

String secondThreadResponse = futures.get(1).get();
assertTrue("slow thread".equals(secondThreadResponse));

5. Verwenden vonExecutorCompletionService

Ein anderer Ansatz zum Ausführen mehrerer Threads ist die Verwendung vonExecutorCompletionService.. Zum Ausführen von Aufgaben werden die angegebenenExecutorService verwendet.

Ein Unterschied zuinvokeAll() ist die Reihenfolge, in der dieFutures,, die die ausgeführten Aufgaben darstellen, zurückgegeben werden. ExecutorCompletionService uses a queue to store the results in the order they are finished, währendinvokeAll() eine Liste mit derselben Reihenfolge zurückgibt, die vom Iterator für die angegebene Aufgabenliste erstellt wurde:

CompletionService service
  = new ExecutorCompletionService<>(WORKER_THREAD_POOL);

List> callables = Arrays.asList(
  new DelayedCallable("fast thread", 100),
  new DelayedCallable("slow thread", 3000));

for (Callable callable : callables) {
    service.submit(callable);
}

Auf die Ergebnisse kann mit der Methodetake()zugegriffen werden:

long startProcessingTime = System.currentTimeMillis();

Future future = service.take();
String firstThreadResponse = future.get();
long totalProcessingTime
  = System.currentTimeMillis() - startProcessingTime;

assertTrue("First response should be from the fast thread",
  "fast thread".equals(firstThreadResponse));
assertTrue(totalProcessingTime >= 100
  && totalProcessingTime < 1000);
LOG.debug("Thread finished after: " + totalProcessingTime
  + " milliseconds");

future = service.take();
String secondThreadResponse = future.get();
totalProcessingTime
  = System.currentTimeMillis() - startProcessingTime;

assertTrue(
  "Last response should be from the slow thread",
  "slow thread".equals(secondThreadResponse));
assertTrue(
  totalProcessingTime >= 3000
  && totalProcessingTime < 4000);
LOG.debug("Thread finished after: " + totalProcessingTime
  + " milliseconds");

awaitTerminationAfterShutdown(WORKER_THREAD_POOL);

6. Fazit

Abhängig vom Anwendungsfall haben wir verschiedene Optionen, um zu warten, bis die Ausführung der Threads abgeschlossen ist.

EinCountDownLatch ist nützlich, wenn wir einen Mechanismus benötigen, um einen oder mehrere Threads zu benachrichtigen, dass eine Reihe von Operationen, die von anderen Threads ausgeführt werden, abgeschlossen ist.

ExecutorCompletionService ist nützlich, wenn wir so schnell wie möglich auf das Aufgabenergebnis zugreifen müssen, und andere Ansätze, wenn wir warten möchten, bis alle laufenden Aufgaben abgeschlossen sind.

Der Quellcode für den Artikel istover on GitHub verfügbar.