ExecutorService
Java Concurrency
Multithreading
Task Completion
Java Programming

ExecutorService, how to wait for all tasks to finish

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

ExecutorService in Java is a powerful framework for managing and controlling threads. It provides a high-level API for handling concurrent execution of tasks. This article will explore details about ExecutorService, focusing specifically on how to wait for all tasks to complete execution.

Introduction to ExecutorService

ExecutorService is part of the java.util.concurrent package introduced in Java 5. It simplifies the execution of asynchronous tasks by managing a pool of worker threads. Unlike manually creating and managing threads, ExecutorService provides a more efficient and less error-prone approach.

Key Features

  • Thread Reuse: Reuses existing worker threads, minimizing resource consumption.
  • Lifecycle Management: Handles thread creation and termination elegantly.
  • Task Submission: Supports the submission of various task types, including Runnable and Callable.
  • Future Interface: Provides a mechanism to retrieve the result of an asynchronous task.

Waiting for All Tasks to Finish

One of the common requirements when using ExecutorService is to wait for all submitted tasks to complete execution. Java's concurrency utilities provide several ways to achieve this.

Methods for Waiting

  1. Using invokeAll
    The invokeAll method takes a collection of tasks and blocks until all tasks are finished. It returns a list of Future objects.
java
    ExecutorService executor = Executors.newFixedThreadPool(3);
    List<Callable<Object>> tasks = //... initialize task list
    List<Future<Object>> futures = executor.invokeAll(tasks);

The key advantage of this method is its simplicity and directness. However, it only works with Callable tasks.

  1. Using shutdown and awaitTermination
    This approach involves shutting down the executor and then waiting for its termination. This is useful for scenarios where you need to ensure all tasks are completed before the application proceeds.
java
    executor.shutdown();
    executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
  • shutdown(): Initiates an orderly shutdown in which previously submitted tasks are executed but no new tasks will be accepted.
  • awaitTermination(): Blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted.
  1. Manually Checking Future
    For more control, you can iterate over the collection of Future objects and check if each task is done.
java
    for (Future<Object> future : futures) {
        future.get(); // blocks until task is done
    }

Example

Below is an example that demonstrates the use of invokeAll to wait for tasks to complete:

java
1import java.util.concurrent.*;
2
3public class Example {
4    public static void main(String[] args) throws InterruptedException, ExecutionException {
5        ExecutorService executor = Executors.newFixedThreadPool(5);
6        List<Callable<String>> tasks = Arrays.asList(
7                new Task("Task 1"), new Task("Task 2"), new Task("Task 3")
8        );
9
10        List<Future<String>> futures = executor.invokeAll(tasks);
11        
12        for (Future<String> future : futures) {
13            System.out.println("Result: " + future.get());
14        }
15
16        executor.shutdown();
17    }
18}
19
20class Task implements Callable<String> {
21    private final String name;
22
23    Task(String name) {
24        this.name = name;
25    }
26
27    @Override
28    public String call() throws Exception {
29        return name + " completed";
30    }
31}

Summary Table

MethodDescriptionUse Cases
invokeAllBlocks until all tasks are finished. Returns a list of Future.Ideal for batch processing of Callable tasks.
shutdown + awaitTerminationInitiates shutdown and waits for completion.Use when a graceful shutdown is required.
Manual Future CheckingIterates over Future objects to ensure completion.Offers fine-grained control over task completion.

Additional Subtopics

Handling Exceptions

When waiting for tasks to finish, it's crucial to handle exceptions. The invokeAll method doesn't throw exceptions directly; instead, they are captured in the Future object:

java
1try {
2    for (Future<String> future : futures) {
3        try {
4            System.out.println("Result: " + future.get());
5        } catch (ExecutionException e) {
6            System.err.println("Task execution failed: " + e.getCause());
7        }
8    }
9} catch (InterruptedException e) {
10    Thread.currentThread().interrupt();
11    // Handle the interruption
12}

Using Custom Thread Pools

ExecutorService can be customized using factory methods from the Executors class — for example, newFixedThreadPool, newCachedThreadPool, and newSingleThreadExecutor. Choosing the appropriate pool type is crucial for both performance and resource management.

Task Cancellation

Task cancellation is supported via the Future interface. Calling cancel(true) attempts to stop the task. It's important to design tasks for responsiveness to interruptions.

Conclusion

ExecutorService provides robust support for parallel processing with built-in capabilities to handle task completion. By leveraging methods like invokeAll and awaitTermination, developers can efficiently manage and wait for tasks in multithreaded applications. Proper understanding of these methods and constructs ensures better concurrency control and application performance.


Course illustration
Course illustration

All Rights Reserved.