Java
Multithreading
Thread Management
Concurrency
Java Programming

Java Wait for thread to finish

Interview Questions practice on Codemia

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

Browse interview questions

Introduction

If you need to wait for a Java thread to finish, the most direct tool is Thread.join(). The broader answer is that waiting strategy depends on how the work was started: raw threads use join, executor tasks use Future.get, and coordinated groups often use synchronization utilities such as CountDownLatch.

Use join() for a Plain Thread

join() blocks the current thread until the target thread has completed.

java
1public class Demo {
2    public static void main(String[] args) throws InterruptedException {
3        Thread worker = new Thread(() -> {
4            try {
5                Thread.sleep(1000);
6            } catch (InterruptedException e) {
7                Thread.currentThread().interrupt();
8            }
9            System.out.println("worker done");
10        });
11
12        worker.start();
13        worker.join();
14        System.out.println("main continues");
15    }
16}

That is the standard answer when you created the thread yourself.

Use a Timed Join if Waiting Forever Is Unsafe

Sometimes an unbounded wait is not acceptable.

java
1worker.join(2000);
2if (worker.isAlive()) {
3    System.out.println("worker still running");
4}

This gives you a fallback path instead of letting the caller block forever.

Use Future.get() for Executor Work

If you submitted the task to an executor, waiting on the returned Future is usually the correct abstraction.

java
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3import java.util.concurrent.Future;
4
5public class Demo {
6    public static void main(String[] args) throws Exception {
7        ExecutorService pool = Executors.newSingleThreadExecutor();
8        Future<Integer> result = pool.submit(() -> 42);
9
10        System.out.println(result.get());
11        pool.shutdown();
12    }
13}

This is better than trying to reach inside the executor and work with raw thread objects.

Use CountDownLatch for Multiple Workers

If several workers must finish before one thread proceeds, CountDownLatch can express that clearly.

java
1import java.util.concurrent.CountDownLatch;
2
3public class Demo {
4    public static void main(String[] args) throws InterruptedException {
5        CountDownLatch latch = new CountDownLatch(2);
6
7        new Thread(() -> { latch.countDown(); }).start();
8        new Thread(() -> { latch.countDown(); }).start();
9
10        latch.await();
11        System.out.println("all workers done");
12    }
13}

This is often clearer than calling join() on many thread references manually.

Handle Interruptions Properly

Both join() and await() can throw InterruptedException. Do not swallow that exception casually. Either propagate it or restore the interrupted status.

That matters because interruption is part of Java’s thread coordination model, not just an inconvenient checked exception.

Waiting Is Coordination, Not Just Blocking

The best waiting primitive is the one that matches the ownership model of the work. If the code uses raw threads, join() is right. If it uses an executor, futures are right. Choosing the correct abstraction keeps concurrency code easier to understand later.

Avoid Waiting in the Wrong Thread

In UI or request-handling code, blocking the current thread can be just as problematic as forgetting to wait at all. Make sure the thread doing the waiting is the one that can safely block, otherwise the program stays correct but becomes unresponsive.

Make Completion Ownership Explicit

Good concurrent code makes it obvious which thread or component owns the responsibility to wait. Hidden waits buried inside utility methods often make timing and shutdown behavior harder to reason about later.

Common Pitfalls

The biggest mistake is using Thread.sleep() in the main thread as a fake way to “wait” for work to finish. Sleep guesses. join() and other coordination APIs actually synchronize on completion.

Another issue is calling join() on executor-managed work when a Future is the real control surface.

A third problem is ignoring interrupts, which makes shutdown and cancellation logic harder to reason about.

Summary

  • Use Thread.join() to wait for a raw thread you started directly.
  • Use timed join when an indefinite block is risky.
  • Use Future.get() for executor-submitted work.
  • Use CountDownLatch when several workers must complete before continuing.
  • Handle InterruptedException deliberately rather than hiding it.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the 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.