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
RunnableandCallable. - 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
- Using
invokeAllTheinvokeAllmethod takes a collection of tasks and blocks until all tasks are finished. It returns a list ofFutureobjects.
The key advantage of this method is its simplicity and directness. However, it only works with Callable tasks.
- Using
shutdownandawaitTerminationThis 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.
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.
- Manually Checking
FutureFor more control, you can iterate over the collection ofFutureobjects and check if each task is done.
Example
Below is an example that demonstrates the use of invokeAll to wait for tasks to complete:
Summary Table
| Method | Description | Use Cases |
invokeAll | Blocks until all tasks are finished. Returns a list of Future. | Ideal for batch processing of Callable tasks. |
shutdown + awaitTermination | Initiates shutdown and waits for completion. | Use when a graceful shutdown is required. |
Manual Future Checking | Iterates 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:
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.

