ExecutorService - Warten auf das Ende der Threads

1. Übersicht

Das ExecutorService- 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 einen ExecutorService ordnungsgemäß herunterfahren und warten, bis bereits ausgeführte Threads ihre Ausführung abgeschlossen haben.

2. Nach dem Herunterfahren des Executors

Wenn Sie einen Executor verwenden, können Sie ihn herunterfahren, indem Sie die Methoden shutdown () oder shutdownNow () aufrufen . Es wird jedoch nicht warten, bis alle Threads nicht mehr ausgeführt werden.

Mit der Methode awaitTermination () können Sie warten, bis vorhandene Threads ihre Ausführung abgeschlossen haben .

Dadurch wird der Thread blockiert, 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 von CountDownLatch

Schauen wir uns als nächstes einen anderen Ansatz zur Lösung dieses Problems an - die Verwendung eines CountDownLatch , 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 await () -Methode aufgerufen haben .

Wenn der aktuelle Thread beispielsweise warten muss, bis weitere N Threads ihre Ausführung beendet haben, können wir den Latch mit N initialisieren :

ExecutorService WORKER_THREAD_POOL = Executors.newFixedThreadPool(10); CountDownLatch latch = new CountDownLatch(2); for (int i = 0; i  { 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 von invokeAll ()

Der erste Ansatz, mit dem wir Threads ausführen können, ist die invokeAll () -Methode. Die Methode gibt eine Liste zukünftiger Objekte zurück, nachdem alle Aufgaben abgeschlossen sind oder das Zeitlimit abgelaufen ist .

Außerdem müssen wir beachten, dass die Reihenfolge der zurückgegebenen Future- Objekte mit der Liste der bereitgestellten Callable- 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 von ExecutorCompletionService

Ein anderer Ansatz zum Ausführen mehrerer Threads ist die Verwendung von ExecutorCompletionService. Es verwendet einen bereitgestellten ExecutorService , um Aufgaben auszuführen.

Ein Unterschied zu invokeAll () besteht in der Reihenfolge, in der die Futures, die die ausgeführten Aufgaben darstellen, zurückgegeben werden. ExecutorCompletionService verwendet eine Warteschlange, um die Ergebnisse in der Reihenfolge zu speichern, in der sie fertig sind , während invokeAll () eine Liste mit derselben sequentiellen 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 Methode take () 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 = 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 darauf zu warten, dass Threads ihre Ausführung beenden.

Ein CountDownLatch 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 ausgeführten Aufgaben abgeschlossen sind.

Der Quellcode für den Artikel ist auf GitHub verfügbar.