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.
Introduction
When you submit work to an ExecutorService, the tasks start running asynchronously and your main thread keeps moving. If later code depends on those tasks being finished, you need an explicit coordination point. Java gives you several good options, and the best one depends on whether you only care about completion or also need the results.
The Basic Pattern: shutdown() Plus awaitTermination()
If you just want to wait until all submitted tasks finish, the standard approach is:
- submit the tasks
- call
shutdown() - call
awaitTermination()
Why this works:
- '
shutdown()stops new tasks from being submitted.' - already submitted tasks are still allowed to run
- '
awaitTermination()blocks the caller until the pool is done or a timeout expires'
This is the right tool when you want a clear "wait here until everything is finished" point.
When You Need Results: Keep the Future Objects
If each task returns a value, capture the Future returned by submit(). Waiting on each future guarantees completion and also gives you each result.
future.get() blocks until that task completes. If the task failed, get() throws an ExecutionException, which is exactly what you want when failure should not be silently ignored.
invokeAll() for a Batch of Callable Tasks
When you already have a collection of Callable tasks, invokeAll() is often the cleanest solution. It submits the whole batch and waits until all of them finish.
This is especially nice when the tasks are already represented as a collection and the caller naturally wants to wait for the entire group.
Which Option Should You Use?
Use shutdown() plus awaitTermination() when:
- tasks are fire-and-forget
- you care about completion, not return values
- you want one shutdown point for the whole pool
Use stored Future objects when:
- each task produces a result
- task failures must be surfaced
- you need per-task control
Use invokeAll() when:
- you have a batch of
Callabletasks - waiting for the whole group is the main goal
These approaches can also be combined. For example, you can keep futures for results and still call shutdown() once all submissions are complete.
Common Pitfalls
One common mistake is calling awaitTermination() without calling shutdown() first. If the pool is still accepting new work, termination may never happen.
Another problem is ignoring interruption. If awaitTermination() or future.get() throws InterruptedException, restore the interrupt status with Thread.currentThread().interrupt() unless you have a clear reason not to.
Some code calls shutdownNow() immediately, expecting it to wait. It does not. It attempts to interrupt running tasks and returns quickly, so it is not the normal waiting mechanism.
Timeouts also matter. Waiting forever can hide deadlocks or hung tasks. In production systems, use a sensible timeout and log or handle the failure path explicitly.
Finally, do not forget that submit() wraps exceptions inside the future. If you never inspect the future, task failures can go unnoticed.
Summary
- The usual way to wait for all
ExecutorServicetasks isshutdown()followed byawaitTermination(). - If tasks return values, keep the
Futureobjects and callget(). - '
invokeAll()is a clean batch API for collections ofCallabletasks.' - Always handle timeouts and interruption deliberately.
- Waiting for completion and collecting results are related, but they are not the same requirement.

