How to make main thread wait for all child threads finish?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Making the main thread wait for worker threads is a standard synchronization task. The simplest version is to start several threads, let them do their work, and then block the main thread until they all finish, but the best implementation depends on whether you created raw threads directly or you are using a thread pool or task framework.
The Classic Answer: join()
When you own the Thread objects directly, the usual solution is join(). It blocks the calling thread until the target thread completes.
Here is a basic Java example:
The main thread reaches the final print only after all three workers are done.
Why join() Works Well
join() is straightforward when:
- the number of child threads is small
- you created them manually
- the control flow is simple
It is easy to read and perfectly appropriate for many programs. The main limitation is that it does not scale gracefully to more complex orchestration patterns.
Waiting for Many Tasks with Executors
If you are using a thread pool, waiting on raw thread objects is often the wrong abstraction. A better pattern is to submit tasks and wait on Future objects:
This is often cleaner because the waiting logic matches the task abstraction already used by the codebase.
Coordination Tools Beyond join()
Other synchronization tools exist for more complex cases:
- '
CountDownLatchwhen several workers must signal completion' - '
CyclicBarrierwhen threads must meet at a common phase' - '
CompletableFuture.allOf()for asynchronous task composition'
For example, CountDownLatch is useful when child threads are created in different parts of the program and the main thread needs a single "all done" signal.
Timeouts and Failure Handling
Waiting forever is often a bad default. Threads can deadlock, block on I/O, or fail silently. If the application cannot tolerate indefinite blocking, use timeouts:
For executor-based code, use bounded waits or cancellation-aware patterns. Also decide what "finished" means when a child thread throws an exception. Completion and success are not the same thing.
Common Pitfalls
The biggest mistake is starting threads and then assuming they will finish before the main thread exits. Without explicit waiting, the program may terminate early or continue before results are ready.
Another mistake is joining from the wrong place. If a UI thread or request thread blocks unnecessarily, the application can become unresponsive.
People also forget interruption handling. If join() is interrupted, the code should usually restore the interrupt status or handle shutdown deliberately.
Finally, if you are already using a high-level concurrency API, avoid dropping down to raw thread management unless there is a real reason. Mixing abstractions often makes code harder to maintain.
Summary
- Use
join()when you created raw child threads and want the main thread to wait for them. - Use executor or task-based waiting when the program already uses a thread pool.
- Consider latches or futures for more complex coordination.
- Prefer bounded waits when indefinite blocking is risky.
- Waiting for completion is simple, but handling failure and cancellation correctly is just as important.

