Joining Threads
When you fire off multiple threads, you sometimes need to wait for one (or more) of them to finish before moving on. That is exactly what Thread.join() does — it pauses the calling thread until the target thread completes.
Why You Need join()
Imagine downloading three files in parallel. After the downloads finish, you want to merge the results. Without join(), your merge code might run before the downloads complete, giving you empty or partial data. join() is the clean, built-in way to express “wait for that thread, then continue.”
public class WithoutJoin {
public static void main(String[] args) throws InterruptedException {
Thread downloader = new Thread(() -> {
System.out.println("Downloading...");
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Download complete.");
});
downloader.start();
// Without join(), "Merging" may print before download finishes
System.out.println("Merging results...");
}
}
Output (likely order without join):
Merging results...
Downloading...
Download complete.
The merge fires too early. Let’s fix that.
Basic Usage of join()
Call join() on the thread object you want to wait for. The current thread (the one calling join()) blocks until the target thread dies.
public class BasicJoin {
public static void main(String[] args) throws InterruptedException {
Thread downloader = new Thread(() -> {
System.out.println("Downloading...");
try { Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Download complete.");
});
downloader.start();
downloader.join(); // main thread waits here
System.out.println("Merging results..."); // runs only after download finishes
}
}
Output:
Downloading...
Download complete.
Merging results...
Note:
join()throwsInterruptedException, so you must handle it. If the waiting thread is interrupted while blocked injoin(), the exception is thrown immediately.
The Three join() Overloads
Thread provides three versions:
| Method | Behaviour |
|---|---|
join() | Wait indefinitely until the thread finishes |
join(long millis) | Wait at most millis milliseconds |
join(long millis, int nanos) | Wait at most millis ms + nanos nanoseconds |
The timed variants are important in production code — you usually do not want to block forever.
public class TimedJoin {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
try { Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Worker done.");
});
worker.start();
worker.join(1500); // wait up to 1.5 seconds
if (worker.isAlive()) {
System.out.println("Worker is still running; moving on anyway.");
} else {
System.out.println("Worker finished within the timeout.");
}
}
}
Output:
Worker is still running; moving on anyway.
Tip: Always check
isAlive()after a timedjoin()to know whether the thread actually finished or the timeout expired.
Joining Multiple Threads
A common pattern is to start several threads, collect them in an array or list, then join them all in a loop.
public class MultiJoin {
public static void main(String[] args) throws InterruptedException {
int count = 4;
Thread[] threads = new Thread[count];
for (int i = 0; i < count; i++) {
final int id = i;
threads[i] = new Thread(() -> {
System.out.println("Thread " + id + " working...");
try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println("Thread " + id + " done.");
});
threads[i].start();
}
// Wait for all threads to complete
for (Thread t : threads) {
t.join();
}
System.out.println("All threads finished. Aggregating results.");
}
}
Output (order of “working” lines may vary):
Thread 0 working...
Thread 1 working...
Thread 2 working...
Thread 3 working...
Thread 0 done.
Thread 1 done.
Thread 2 done.
Thread 3 done.
All threads finished. Aggregating results.
Tip: For larger-scale coordination, look at
CountDownLatchorCompletableFuturefromjava.util.concurrent— they compose better when you have many tasks.join()is perfect for simple, small sets of threads.
join() and the Thread Life Cycle
Understanding where join() fits in the thread life cycle helps avoid confusion. When you call join():
- The calling thread moves from Runnable to Waiting (or Timed Waiting with a timeout).
- It stays there until the target thread reaches the Terminated state.
- The JVM then wakes the waiting thread, which returns to Runnable.
If the target thread is already terminated when you call join(), the method returns immediately — no blocking at all.
public class JoinAlreadyDone {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> System.out.println("Quick task"));
t.start();
t.join(); // wait until terminated
t.join(); // called again — returns immediately, thread is already dead
System.out.println("State: " + t.getState()); // TERMINATED
}
}
Output:
Quick task
State: TERMINATED
Handling InterruptedException Correctly
Do not silently swallow the InterruptedException. The right pattern is to restore the interrupted flag so that callers further up the stack can react:
public class SafeJoin {
public static void waitFor(Thread t) {
try {
t.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // restore flag
System.out.println("Interrupted while waiting for thread: " + t.getName());
}
}
public static void main(String[] args) {
Thread worker = new Thread(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "worker-1");
worker.start();
waitFor(worker);
System.out.println("Finished waiting.");
}
}
Warning: Catching
InterruptedExceptionand doing nothing (empty catch block) hides the signal. Always re-interrupt or rethrow.
Under the Hood
Internally, Thread.join() is implemented using Object.wait() on the Thread object itself. Here is a simplified view of the JDK source:
// Simplified — actual JDK source is in java.lang.Thread
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
while (isAlive()) {
if (millis == 0) {
wait(0); // wait indefinitely on this Thread's monitor
} else {
long delay = millis - now;
if (delay <= 0) break;
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
When a thread terminates, the JVM calls notifyAll() on the thread object’s monitor — that is what wakes up any threads blocked in join(). This is why you should never call wait(), notify(), or notifyAll() directly on a Thread object in your own code — you could interfere with this mechanism and break join().
The method is synchronized, which means only one thread can be in the join() method on a given Thread instance at a time. Multiple threads can all join() the same target thread, though — the while (isAlive()) loop handles spurious wakeups and ensures all waiters see the termination.
join() vs sleep() vs yield()
| Method | Who it affects | Typical use |
|---|---|---|
join() | Caller waits for a specific thread to die | Coordinate results between threads |
sleep(ms) | Caller pauses for a fixed time | Rate limiting, retries, simulations |
yield() | Hints the scheduler to switch away | Rarely useful in practice |
See Thread.sleep() for a deeper look at pausing execution without waiting on another thread.
Quick Reference
Thread t = new Thread(task);
t.start();
t.join(); // wait forever
t.join(2000); // wait up to 2 seconds
t.join(2000, 500_000); // wait up to 2 seconds + 500,000 nanoseconds
boolean stillRunning = t.isAlive(); // check after timed join
Related Topics
- Thread Life Cycle — understand the states a thread moves through, including WAITING and TERMINATED
- Creating a Thread — how to define and start threads before you join them
- Thread.sleep() — pause a thread for a fixed duration instead of waiting on another thread
- Callable & Future — a higher-level alternative that returns results from threads and supports timeout-based waiting
- Synchronization — coordinate shared data access across threads running concurrently
- Inter-Thread Communication —
wait()/notify()patterns for threads to signal each other