How to wait for all threads to finish, using ExecutorService?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
In Java, managing multiple threads manually can be a complex and error-prone task. The ExecutorService framework, part of the java.util.concurrent package, is a robust solution for handling concurrent tasks. It provides a higher level of abstraction for running tasks asynchronously and facilitates the management of thread life cycles. One common requirement when working with threads is ensuring that all threads have completed their execution. ExecutorService provides several mechanisms to achieve this.
Understanding ExecutorService
To begin, an instance of ExecutorService can be created using the various factory methods in the Executors class. The ExecutorService creates and manages a pool of threads where tasks submitted for execution are run.
Here is a basic example of how to create an ExecutorService:
In this example, a fixed thread pool with four threads is created. You can adjust the number of threads based on the computational resources and requirements of your application.
Submitting Tasks to the ExecutorService
Tasks that need to be executed in parallel are submitted to the ExecutorService using the execute(Runnable) or submit(Callable) methods. The Callable tasks are similar to Runnable, but they can return a result and throw a checked exception. Here's an example of submitting tasks:
Waiting for Completion of All Tasks
To wait for all threads managed by an ExecutorService to finish executing all tasks, you can use the shutdown() method followed by awaitTermination(). Here’s how these methods work:
- shutdown() - This method initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
- awaitTermination(long timeout, TimeUnit unit) - This method blocks until all tasks have completed execution after a shutdown request, or the timeout occurs, or the current thread is interrupted, whichever happens first.
Here is a practical example to demonstrate this:
Key Points Summary
| Method | Purpose |
shutdown() | Initiates an orderly shutdown; executed tasks are run, no new are taken. |
shutdownNow() | Attempts to stop all actively executing tasks and halts processing of waiting tasks. |
awaitTermination() | Blocks until all tasks have completed after a shutdown request. |
Considerations and Best Practices
- Choosing the Right Number of Threads: The optimal number of threads depends on the number of available CPU cores and the nature of tasks (CPU-bound vs I/O-bound).
- Handling InterruptedException: Always consider the thread's interrupt status when handling
InterruptedExceptionand restore the interrupt status after catching such exceptions. - Resource Management: It's critical to always shutdown the
ExecutorService. If neglected, threads might continue running and lead to memory leaks.
Conclusion
Using ExecutorService for managing the concurrent execution of multiple tasks in Java offers a clean and manageable approach. The provided shutdown mechanisms ensure that we can control thread execution effectively, allowing for a graceful completion of all tasks. By understanding and properly utilizing these tools, developers can improve the reliability and performance of their multi-threaded applications.

