skip to content

What does Thread.join() do, and when would you use it?

level: juniorimportance: must knowfreq 72%

answer

  1. join = wait for that thread to die
  2. caller blocks in WAITING / TIMED_WAITING
  3. timed join may return early → check isAlive()
  4. join establishes happens-before (results visible)
  5. throws InterruptedException; join(0) = wait forever

basics

~10 s

Calling t.join() makes the current thread wait until thread t finishes before continuing. You use it to make sure a worker thread's work is done before reading its results.

solid answer

~40 s

Thread.join() lets one thread wait for another to terminate. If thread A calls b.join(), A blocks (in WAITING, or TIMED_WAITING if a timeout is given) until thread B's run() completes — by returning, throwing, or being terminated. join() is built on wait/notify internally: the JVM notifies waiters when the thread dies. The overloads join(millis) and join(millis, nanos) wait at most that long, then return regardless of whether the thread finished — so after a timed join you should check isAlive() to know if it actually completed. join() throws InterruptedException. Typical use: a main thread spawns workers, then joins each so it can safely read their results, knowing the workers' writes happen-before the join returns (memory visibility is guaranteed). Joining a thread that has already finished returns immediately.

code

java · 11 lines
java
Thread worker = new Thread(() -> {
    // compute something
});
worker.start();

worker.join(2000);            // wait up to 2s
if (worker.isAlive()) {
    // timed out: worker still running
} else {
    // worker finished; its results are safely visible here
}

go deeper

for a junior

Knows t.join() makes the current thread wait until t finishes, used to sequence work.

for a middle

Explains WAITING vs TIMED_WAITING, the timeout overloads + isAlive() check, and that join throws InterruptedException.

for a senior

Articulates the happens-before/memory-visibility guarantee, that join is built on wait/notify, the join(0) gotcha, and that interrupting the joiner doesn't stop the target.

for a principal

Compares raw join to ExecutorService/Future/CompletableFuture/structured concurrency, reasons about cancellation, error propagation, and scaling coordination beyond a handful of threads.

## The problem join solves When you start a thread with `t.start()`, it runs **concurrently** — your code keeps going while the new thread does its work. Often you need to **wait for that thread to finish** before moving on (e.g., to combine its results). `Thread.join()` is exactly that: "block me until that other thread has ended." ## What join does, precisely `t.join()` is an instance method called on a **target thread** `t`. The thread that *calls* it (the caller) is suspended until `t` **terminates** — i.e., `t`'s `run()` method returns or throws, putting `t` in the `TERMINATED` state. While waiting, the caller is in the **`WAITING`** state (or **`TIMED_WAITING`** if you pass a timeout). ```java Thread worker = new Thread(() -> doWork()); worker.start(); worker.join(); // main thread blocks here until worker ends useResultsOf(worker); // safe: worker has finished ``` ## Overloads and the timeout - `join()` — wait **indefinitely** until the thread dies. - `join(long millis)` — wait **at most** `millis`; if the thread is still running when time runs out, `join` simply **returns anyway**. - `join(long millis, int nanos)` — finer-grained timeout. Because a **timed** join can return *before* the thread finishes, you must check **`t.isAlive()`** afterward to know whether it actually completed or you just timed out. (A plain untimed `join()` always means the thread is done when it returns.) ## How it works under the hood `join` is implemented with **`wait/notify`** on the thread object. The thread's monitor is used internally; when a thread terminates, the JVM performs a `notifyAll` on it, waking any joiners. Note a subtle pitfall: because `Thread` itself uses its own monitor for `join`, you should **never** call `wait`/`notify` on a `Thread` object yourself — you'd interfere with `join`'s mechanism. ## Memory visibility — a key guarantee `join` establishes a **happens-before** relationship: everything the joined thread did **before it terminated** is **guaranteed visible** to the caller **after** `join` returns. So you can read the worker's results without extra synchronization. This is one of the main reasons join is safe and useful, not just convenient. ## Interruption `join` throws **`InterruptedException`**: another thread can `interrupt()` the waiter to make it stop waiting early. You must handle or propagate that checked exception. Interrupting the *joiner* does not stop the *target* thread — it only ends the wait. ## Edge cases - Joining a thread that **already finished** returns immediately. - Joining a thread that was **never started** also returns immediately (it's considered not alive). - `join(0)` means "wait forever," the same as `join()` (0 = no timeout, not zero wait). ## Modern alternatives For coordinating *many* tasks, raw `join` gets clumsy. Prefer `ExecutorService` + `Future.get()`, `CompletableFuture`, `CountDownLatch`, or (Java 21+) **structured concurrency** (`StructuredTaskScope`) which joins a whole group of tasks cleanly.

  • After t.join(5000) returns, how do you know whether t actually finished?
    Check t.isAlive(). A timed join returns when the thread dies OR when the timeout elapses, so isAlive() distinguishes 'finished' (false) from 'timed out, still running' (true).
  • Does join() guarantee you can safely read data the joined thread wrote?
    Yes. join establishes a happens-before edge: all writes the thread made before terminating are visible to the caller after join returns, so no extra synchronization is needed to read those results.

saying these in an interview costs you the question

  • Thinking join() stops or kills the target thread — it only waits for it to finish naturally.
  • Assuming a timed join() means the thread finished — it may have just timed out; check isAlive().
  • Believing join(0) waits zero time — 0 means wait indefinitely.
  • Forgetting the happens-before visibility guarantee and adding redundant synchronization, or worse, reading results before joining.

context