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.
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.
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
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) andalive after join? true. That's a race: the output depends on timing. - 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
Expected output
before start: NEW
waiting on lock: WAITING
worker: interrupted while waiting
after join: TERMINATEDExpected output
summer computed 500500
alive after join? falseExpected output
ticker stopped cleanly
main: doneBreak 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();.
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.
- ▸
joinis implemented withwait/notifyAllon the Thread object itself (the JVM callsnotifyAllon it when the thread terminates), which is why you should never use aThreadinstance as your own lock object. - ▸
A thread blocked in classic I/O (
InputStream.readon 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
Thread.getState()returns one of the sixThread.Statevalues. 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 asynchronizedblock 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
**
start()may be called once**. Calling it again throwsIllegalThreadStateException, even after the thread finished. A thread can't be restarted; create a new one (or use a pool, Topic 13.6). - 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 fromjoin(a happens-before edge, Topic 13.4). That's why reading a worker's result afterjoinis safe. - 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. CallingotherThread.sleep()still sleeps the current thread;sleepis static. - 5
Interruption is cooperative.
t.interrupt()sets a flag ont. Iftis blocked insleep,waitorjoin, that call throwsInterruptedExceptionand clears the flag. Running code checksThread.currentThread().isInterrupted()and stops on its own. Java has no safe way to kill a thread (Thread.stopis deprecated and removed in recent JDKs because it left objects half-updated). - 6
Never swallow
InterruptedExceptionsilently. Either let it propagate (declarethrows InterruptedException) or, if you can't, restore the flag withThread.currentThread().interrupt()so code higher up still sees that a stop was requested.
Explain it without notes
Name the six thread states and give one action that puts a thread into each.
What does join guarantee besides waiting?
How should code respond to InterruptedException, and why?
Practice
Start three threads that each compute part of a sum (1..300 split into three ranges), join them all, and print the total.
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.