Thread.join() in Java
Fast recall
Quick revision
Core answer
Understanding of thread coordination
Not knowing join() throws InterruptedException
Explain the timeout version join(ms) and show a practical use case with multiple threads
Interview-ready guide
Interview answer
Thread.join() makes the current thread wait for another thread to terminate. In worker.join(), worker is the target and the thread executing that line is the caller. A no-timeout join puts the caller in WAITING while the target is alive; it does not pause the target or make the caller perform the target's work.
Use join() when the next step depends on completed worker work. For an order batch, start every worker first, then run worker.join() for each one, and build the report only after those joins return. Starting and immediately joining one worker at a time would be correct, but it would remove the intended parallelism.
A successful return from join() also provides a memory-visibility guarantee: every action in the worker happens-before the caller continues after the join. Results written by the worker are therefore visible to the caller. The caller may itself be interrupted while waiting, so InterruptedException should be propagated, or the interrupt status restored when the method cannot throw it.
join(1000) can return because the target finished or because one second elapsed; it does not return a success flag, so check worker.isAlive() when that distinction matters. Circular dependencies such as thread A joining B while B joins A can wait forever. For larger task workflows, Future, ExecutorService, or CompletableFuture usually expresses completion more directly.
Thread.join() in Java
Direct Answer
Thread.join() makes the current thread wait until the thread it's called on terminates. It's used to ensure a thread has completed before the caller proceeds — for example, waiting for all worker threads to finish before aggregating results.
Code Example
public class OrderProcessor {
public static void main(String[] args) throws InterruptedException {
var orders = List.of("ORD-001", "ORD-002", "ORD-003");
var workers = new ArrayList<Thread>();
for (var orderId : orders) {
var worker = new Thread(() -> {
System.out.println("Processing " + orderId);
try {
Thread.sleep(1000); // Simulate work
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Completed " + orderId);
});
worker.start();
workers.add(worker);
}
// Wait for all workers to finish before aggregating
for (var worker : workers) {
worker.join(); // Main thread waits here
}
System.out.println("All orders processed, generating report");
}
}
Common Mistakes & Pitfalls
- Ignoring InterruptedException: Freshers catch it with an empty block or just printStackTrace(), but you should either re-throw it (if your method allows) or call Thread.currentThread().interrupt() to preserve the interrupted status.
- Joining in the wrong order: If thread A joins B and B joins A, you create a deadlock — both wait forever.
- Not using timeout in production: Calling join() without a timeout means if that thread hangs, your calling thread hangs too, potentially blocking critical shutdown or health checks.
Check your understanding
Answer these without looking back at the article.
- What state does the calling thread enter when it calls join()?
- What is the difference between join() and join(1000)?
- What happens if join() is called on a thread that hasn't started?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.