Java – Waiting for Running Threads to Finish

Java concurrency allows running multiple sub-tasks of a task in separate threads. Sometimes, it is necessary to wait until all the threads have finished their execution. In this tutorial, we will learn a few ways to make the current thread wait for the other threads to finish.

1. Using ExecutorService and Future.get()

Java ExecutorService (or ThreadPoolExecutor) helps execute Runnable or Callable tasks asynchronously. Its submit() method returns a Future object that we can use to cancel execution and/or wait for completion.

In following example, we have a demo Runnable task. Each task completes in a random time between 0 to 1 second.

public class DemoRunnable implements Runnable {

  private Integer jobNum;

  public DemoRunnable(Integer index) {
    this.jobNum = index;

  public void run() {
    Thread.sleep(new Random(0).nextLong(1000));
    System.out.println("DemoRunnable completed for index : " + jobNum);

We are submitting 10 tasks to the executor service. And then, we invoke Future.get() method on each Future object as received after submitting the task to the executor. The Future.get() waits if necessary for the task to complete, and then retrieves its result.

ExecutorService executor = Executors.newFixedThreadPool(5);

List<Future<?>> futures = new ArrayList<>();

for (int i = 1; i <= 10; i++) {
  Future<?> f = executor.submit(new DemoRunnable(i));

System.out.println("###### All tasks are submitted.");

for (Future<?> f : futures) {

System.out.println("###### All tasks are completed.");
###### All tasks are submitted.
DemoRunnable completed for index : 3
DemoRunnable completed for index : 4
DemoRunnable completed for index : 1
DemoRunnable completed for index : 5
DemoRunnable completed for index : 2
DemoRunnable completed for index : 6
DemoRunnable completed for index : 10
DemoRunnable completed for index : 7
DemoRunnable completed for index : 9
DemoRunnable completed for index : 8
###### All tasks are completed.

Note that the wait may terminate earlier under the following conditions:

  • the task is cancelled
  • the task execution threw an exception
  • there is an InterruptedException i.e., current thread was interrupted while waiting.

In such a case, we should implement our own logic to handle the exception.

2. Using ExecutorService shutdown() and awaitTermination()

The awaitTermination() method blocks until all tasks have completed execution after a shutdown() request on the executor service. Similar to Future.get(), it can unblock earlier if the timeout occurs, or the current thread is interrupted.

The shutdown() method closes the executor so no new tasks can be submitted, but previously submitted tasks continue execution.

The following method has the complete logic of waiting for all tasks to finish in 1 minute. After that, the executor service will be shut down forcibly using shutdownNow() method.

void shutdownAndAwaitTermination(ExecutorService executorService) {
    try {
        if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
    } catch (InterruptedException ie) {

We can use this method as follows:

ExecutorService executor = Executors.newFixedThreadPool(5);

for (int i = 1; i <= 10; i++) {
  executor.submit(new DemoRunnable(i));

System.out.println("###### All tasks are submitted.");


System.out.println("###### All tasks are completed.");

3. Using ExecutorService invokeAll()

This approach can be seen as a combination of the previous two approaches. It accepts the tasks as a collection and returns a list of Future objects to retrieve output if necessary. Also, it uses the shutdown and awaits logic for waiting for the tasks to be complete.

In following example, we are using the DemoCallable class that is very similar to DemoRunnable, except it returns an Integer value.

ExecutorService executor = Executors.newFixedThreadPool(10);

List<DemoCallable> tasks = Arrays.asList(
    new DemoCallable(1), new DemoCallable(2),
    new DemoCallable(3), new DemoCallable(4),
    new DemoCallable(5), new DemoCallable(6),
    new DemoCallable(7), new DemoCallable(8),
    new DemoCallable(9), new DemoCallable(10));

System.out.println("###### Submitting all tasks.");

List<Future<Integer>> listOfFutures = executor.invokeAll(tasks);


System.out.println("###### All tasks are completed.");

Note that listOfFutures stores the task outputs in the same order in which we had submitted the tasks to the executor service.

for (Future f : listOfFutures) {
  System.out.print(f.get() + " "); //Prints 1 2 3 4 5 6 7 8 9 10 

4. Using CountDownLatch

The CountDownLatch class enables a Java thread to wait until a collection of threads (latch is waiting for) to complete their tasks.

CountDownLatch works by having a counter initialized with a number of threads, which is decremented each time a thread completes its execution. When the count reaches zero, it means all threads have completed their execution, and the main thread waiting on the latch resumes the execution.

In the following example, the main thread is waiting for 3 given services to complete before reporting the final system status. We can read the whole example in CountDownLatch example.

CountDownLatch latch = new CountDownLatch(3);

List<BaseHealthChecker> services = new ArrayList<>();
services.add(new NetworkHealthChecker(latch));
services.add(new CacheHealthChecker(latch));
services.add(new DatabaseHealthChecker(latch));

Executor executor = Executors.newFixedThreadPool(services.size());
for(final BaseHealthChecker s : services) {
//Now wait till all health checks are complete

5. Conclusion

In this tutorial, we learned to make an application thread wait for other threads to finish. We learned to use the ExecutorService methods and CountDownLatch class.

Happy Learning !!

Source Code on Github


Notify of
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial

Dark Mode

Dark Mode