multithreading
concurrency
thread synchronization
parallel programming
thread management

How to wait for a number of threads to complete?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Waiting for several threads to finish is a synchronization problem, not just a looping problem. The simplest answer is often join, but once the code grows beyond a few manually created threads, higher-level coordination tools are usually better. The right choice depends on whether you are dealing with raw threads, a thread pool, or task abstractions that already model completion.

Use join for Manually Created Threads

If you explicitly created Thread objects, the direct way to wait is to start them and then join each one.

java
1public class Main {
2    public static void main(String[] args) throws InterruptedException {
3        Thread t1 = new Thread(() -> work("A"));
4        Thread t2 = new Thread(() -> work("B"));
5        Thread t3 = new Thread(() -> work("C"));
6
7        t1.start();
8        t2.start();
9        t3.start();
10
11        t1.join();
12        t2.join();
13        t3.join();
14
15        System.out.println("All threads completed.");
16    }
17
18    static void work(String name) {
19        try {
20            Thread.sleep(500);
21            System.out.println("Finished " + name);
22        } catch (InterruptedException e) {
23            Thread.currentThread().interrupt();
24        }
25    }
26}

join blocks the calling thread until the target thread finishes. This is fine for a small number of explicitly managed threads.

Use CountDownLatch for Group Completion

When you want "wait until N workers finish" without joining each one directly, CountDownLatch is a better abstraction.

java
1import java.util.concurrent.CountDownLatch;
2
3public class Main {
4    public static void main(String[] args) throws InterruptedException {
5        int workers = 3;
6        CountDownLatch latch = new CountDownLatch(workers);
7
8        for (int i = 0; i < workers; i++) {
9            int workerId = i;
10            new Thread(() -> {
11                try {
12                    Thread.sleep(300);
13                    System.out.println("Worker " + workerId + " done");
14                } catch (InterruptedException e) {
15                    Thread.currentThread().interrupt();
16                } finally {
17                    latch.countDown();
18                }
19            }).start();
20        }
21
22        latch.await();
23        System.out.println("All workers completed.");
24    }
25}

This pattern is useful when worker threads may be created in different places but one coordinator needs to wait for them all.

Prefer Executors and Futures in Real Applications

Raw threads are often the wrong level for application code. If work units come from a pool, use an executor and wait on Future results or use invokeAll.

java
1import java.util.Arrays;
2import java.util.List;
3import java.util.concurrent.Callable;
4import java.util.concurrent.ExecutorService;
5import java.util.concurrent.Executors;
6import java.util.concurrent.Future;
7
8public class Main {
9    public static void main(String[] args) throws Exception {
10        ExecutorService pool = Executors.newFixedThreadPool(3);
11
12        List<Callable<String>> tasks = Arrays.asList(
13                () -> "A",
14                () -> "B",
15                () -> "C"
16        );
17
18        List<Future<String>> futures = pool.invokeAll(tasks);
19
20        for (Future<String> future : futures) {
21            System.out.println("Result: " + future.get());
22        }
23
24        pool.shutdown();
25    }
26}

invokeAll blocks until every submitted task completes, which makes it a strong fit for batch-style thread-pool work.

Handle Timeouts and Interruptions Intentionally

Waiting forever is often a bug. If worker completion matters operationally, use a timeout-aware wait.

With CountDownLatch:

java
1boolean finished = latch.await(2, java.util.concurrent.TimeUnit.SECONDS);
2if (!finished) {
3    System.out.println("Timed out waiting for workers.");
4}

With join:

java
t1.join(2000);

Also remember that blocking waits can throw InterruptedException. Good code either propagates it or restores the interrupt status instead of swallowing it.

Choose the Highest-Level Abstraction You Can

As the design matures, the best answer is usually:

  • 'join for a few raw threads'
  • 'CountDownLatch for group completion'
  • executor and futures for pooled work
  • task frameworks such as CompletableFuture for async composition

If you find yourself manually managing large numbers of threads, that is usually a signal that the program should move to a higher-level concurrency model.

Common Pitfalls

  • Joining threads that were never started.
  • Forgetting to count down the latch in a finally block.
  • Waiting forever without a timeout in code that runs in production workflows.
  • Managing many raw threads manually when an executor would be simpler and safer.
  • Swallowing InterruptedException and losing the thread interruption signal.

Summary

  • 'join is the direct way to wait for explicitly created threads.'
  • 'CountDownLatch is better when you need to wait for a group of workers to finish.'
  • Executors and futures are usually the right abstraction for real application concurrency.
  • Add timeouts when a hang would be operationally harmful.
  • Prefer higher-level concurrency tools over manual thread management as the design grows.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.