Command Palette

Search for a command to run...

PHASE 13Advanced ~18 min· topic 2 of 11

Topic 13.2

Thread Lifecycle, join and sleep

In one line

A thread moves through six states: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED. join waits for another thread to finish, sleep pauses the current one, and interrupt politely asks a thread to stop what it's waiting for.

Think of it like this

Runners in a relay race. Before the race a runner is registered but not running (NEW). On the track they're running (RUNNABLE). Waiting for the baton they're waiting (WAITING), and resting for a fixed two minutes they're timed-waiting (TIMED_WAITING). Stuck behind a closed gate that someone else holds, they're blocked (BLOCKED). After crossing the finish line they're done (TERMINATED). The coach can't drag a runner off the track, but can tap them on the shoulder (interrupt) and the runner decides to stop.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Thread state
Which phase of its life a thread is in, such as running, waiting or finished.
join
Wait until another thread has finished.
sleep
Pause the current thread for some milliseconds.
Interrupt
A polite request to a thread to stop waiting or stop working. The thread decides how to respond.
InterruptedException
The exception thrown by sleep, wait and join when the waiting thread is interrupted.
Blocked
Waiting to get a lock that another thread is holding.
Cooperative
Working only if both sides play along: interruption needs the interrupted code to check and react.

Step by step

01The six states and how a thread moves between them

A thread starts NEW, becomes RUNNABLE on start(), may move into BLOCKED, WAITING or TIMED_WAITING and back to RUNNABLE many times, and ends TERMINATED. There's no way back from TERMINATED.

These are Java's own states for monitoring (they show up in thread dumps from jstack, Topic 14.5), not the operating system's scheduler states.

The six states and how a thread moves between themdiagram
Rendering diagram…

02join: wait for the work to finish

Without join, the main thread might print a result before the worker has written it. With join, the main thread waits in WAITING until the worker is TERMINATED, and the JMM guarantees it sees all of the worker's writes.

join(ms) waits at most that long. Afterwards check isAlive() to know whether the thread really finished or the timeout hit.

03sleep: pause, but keep your locks

Thread.sleep(100) puts the current thread in TIMED_WAITING for about 100 ms. The OS scheduler decides exactly when it runs again.

Sleeping inside a synchronized block keeps the lock, so every other thread that needs it waits too. That's a common cause of slow code. Object.wait() is different: it releases the lock while waiting (Topic 13.5).

04Interrupting a thread that's waiting

If the target is in sleep, wait or join, interrupt() wakes it immediately with InterruptedException. The flag is cleared when the exception is thrown, which is why handlers often call Thread.currentThread().interrupt() to set it again.

If the target is busy computing, nothing happens to it until it checks isInterrupted(). Long loops should check the flag so they can be cancelled.

Main.javawhole filejava
while (!Thread.currentThread().isInterrupted()) {
    try {
        doSomeWork();
        Thread.sleep(10);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();   // restore the flag; the loop condition ends the loop
    }
}

05Why there's no kill switch

Thread.stop() used to kill a thread by throwing an error at whatever line it happened to be on, releasing all its locks halfway through updates and leaving shared objects broken. It was deprecated in Java 1.2 and now throws UnsupportedOperationException (Java 20).

Designing for cancellation means: check the interrupt flag in loops, let blocking calls throw InterruptedException, and clean up in finally.

Try it yourself

  1. 1

    Remove the join

    In "join makes the result safe to read", delete worker.join();. Run it a few times. You may see 0 (the result wasn't written yet) and alive after join? true. That's a race: the output depends on timing.

  2. 2

    Forget to restore the flag

    In "Stopping a thread cooperatively", delete Thread.currentThread().interrupt();. The exception clears the flag, so the loop never ends and the run hits the time limit. This is the bug the restore line prevents.

Code & diagrams

Watch a thread change state New tab
Sign in to run this example in your browser.

Expected output

before start: NEW
waiting on lock: WAITING
worker: interrupted while waiting
after join: TERMINATED
join makes the result safe to read New tab
Sign in to run this example in your browser.

Expected output

summer computed 500500
alive after join? false
Stopping a thread cooperatively New tab
Sign in to run this example in your browser.

Expected output

ticker stopped cleanly
main: done

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Start a thread twice

Call worker.start(); a second time after worker.join();.

terminal
$ java Main.java
── what you'll see ──
Exception in thread "main" java.lang.IllegalThreadStateException
at java.base/java.lang.Thread.start(Thread.java:1525)
at Main.main(Main.java:12)

Myth vs fact

Myth

sleep releases the lock so other threads can use it.

Fact

sleep keeps every lock the thread holds. Only Object.wait() (and Condition.await) release the lock while waiting.

Myth

interrupt() stops a thread.

Fact

It only sets a flag and wakes blocking calls. The thread must check the flag or handle InterruptedException and stop itself.

Myth

RUNNABLE means the thread is running on a CPU right now.

Fact

RUNNABLE covers both running and ready-to-run. Java's states don't show which one.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    join is implemented with wait/notifyAll on the Thread object itself (the JVM calls notifyAll on it when the thread terminates), which is why you should never use a Thread instance as your own lock object.

  • ▸

    A thread blocked in classic I/O (InputStream.read on a socket) doesn't respond to interrupt; close the socket to unblock it. Interruptible channels (java.nio) and virtual threads (Topic 13.10) behave better.

  • ▸

    Thread.onSpinWait() (Java 9) is a hint for busy-wait loops that lets the CPU save power (the PAUSE instruction on x86), but proper waiting (join, latches, conditions) is almost always better than spinning.

Remember this

  1. 1

    Thread.getState() returns one of the six Thread.State values. NEW: created, start() not called yet. RUNNABLE: running or ready to run (Java doesn't distinguish "on a CPU right now" from "waiting for a CPU"). BLOCKED: waiting to enter a synchronized block another thread holds. WAITING: waiting with no time limit (join(), Object.wait(), LockSupport.park()). TIMED_WAITING: the same with a time limit (sleep(ms), join(ms), wait(ms)). TERMINATED: run() has returned or thrown.

  2. 2

    **start() may be called once**. Calling it again throws IllegalThreadStateException, even after the thread finished. A thread can't be restarted; create a new one (or use a pool, Topic 13.6).

  3. 3

    **join() makes the calling thread wait until the other thread terminates. It's also a memory-visibility guarantee**: everything the finished thread wrote is visible to the thread that returns from join (a happens-before edge, Topic 13.4). That's why reading a worker's result after join is safe.

  4. 4

    **Thread.sleep(ms) pauses the current thread for at least that long (it may oversleep, never undersleep). It does not** release locks it holds. Calling otherThread.sleep() still sleeps the current thread; sleep is static.

  5. 5

    Interruption is cooperative. t.interrupt() sets a flag on t. If t is blocked in sleep, wait or join, that call throws InterruptedException and clears the flag. Running code checks Thread.currentThread().isInterrupted() and stops on its own. Java has no safe way to kill a thread (Thread.stop is deprecated and removed in recent JDKs because it left objects half-updated).

  6. 6

    Never swallow InterruptedException silently. Either let it propagate (declare throws InterruptedException) or, if you can't, restore the flag with Thread.currentThread().interrupt() so code higher up still sees that a stop was requested.

Explain it without notes

01

Name the six thread states and give one action that puts a thread into each.

02

What does join guarantee besides waiting?

03

How should code respond to InterruptedException, and why?

Practice

01

Start three threads that each compute part of a sum (1..300 split into three ranges), join them all, and print the total.

02

Write a thread that prints "tick" every 20 ms and is stopped after about 70 ms by interrupt; print only how it ended.

Trade-offs

  • ↔

    join is simple and gives visibility for free, but blocks the waiting thread; futures and latches (Topics 13.6, 13.8) scale better to many tasks.

  • ↔

    Cooperative cancellation needs discipline in every loop, but it's the only way to stop work without corrupting shared state.

Done when you can

  • Done when you can name the six thread states and what moves a thread between them.

  • Done when you use join to wait for results and can explain its visibility guarantee.

  • Done when you can cancel a thread with interrupt and never swallow InterruptedException.

  • Done when you know sleep keeps locks and threads can't be restarted.