Thread Life Cycle Within Concurrency
A thread's "state" is not a property the thread sets on itself — it is a label the runtime and the OS scheduler attach to it based on why it is or isn't holding a CPU right now: a thread is RUNNABLE when it is on the scheduler's ready queue competing for a core, and it leaves that queue (becoming BLOCKED, WAITING, or TIMED_WAITING) the instant it makes a system or library call that cannot proceed, then re-enters the queue when the event it was parked on fires.
So the life cycle is really a description of which kernel/runtime queue a thread sits in over time. Reading it that way — instead of as five vocabulary words — tells you exactly what a thread is doing when you see it stuck in a thread dump, and what has to happen for it to move.
The states and the transitions that drive them
Java's Thread.State enum names six states. The important part is not the names but the edges — every transition is caused by a specific call or scheduler decision, never by the thread "deciding" to change:
| State | What the thread is doing | How it got here |
|---|---|---|
| NEW | Object exists; no OS thread yet | new Thread(r) — nothing scheduled |
| RUNNABLE | On the ready queue or executing on a core | start() creates the OS thread and enqueues it |
| BLOCKED | Parked at a monitor lock it could not acquire | Hit synchronized while another thread owns the monitor |
| WAITING | Parked indefinitely until signalled | wait(), join(), LockSupport.park() |
| TIMED_WAITING | Parked until signalled or a deadline | sleep(ms), wait(ms), poll(timeout) |
| TERMINATED | run() returned or threw; OS thread reclaimed | Exit of the run method |
Note there is no separate "Running" state in the JVM: a thread executing on a core and a thread sitting on the ready queue are both RUNNABLE. "Running" is an OS scheduler distinction (the CPU's run queue vs. the wait queues), not something the JVM exposes — which is exactly why the old five-box "New / Runnable / Running / Blocked / Terminated" picture misleads.
A traced run with real values
Two threads share one counter behind a synchronized block. T-worker does the work; main waits for it with join(). We tag each step with the thread's Thread.State as a thread dump would report it at that instant.
public class Trace {
static int counter = 0;
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
synchronized (lock) { // step 4: may BLOCK if main holds lock
for (int i = 0; i < 3; i++) {
counter++;
try { Thread.sleep(50); } // step 6: TIMED_WAITING, 50 ms
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}
}, "T-worker");
System.out.println(worker.getState()); // step 1
worker.start(); // step 2
worker.join(); // step 5: main WAITING
System.out.println("counter=" + counter + " " + worker.getState());
}
}Timeline (times approximate, single core so only one thread is on-CPU at a time):
| t (ms) | Event | T-worker state | main state |
|---|---|---|---|
| 0 | Object constructed, before start() | NEW | RUNNABLE |
| 1 | worker.start() → OS thread enqueued | RUNNABLE | RUNNABLE |
| 2 | main calls worker.join() | RUNNABLE | WAITING |
| 3 | worker enters synchronized (lock free) | RUNNABLE | WAITING |
| 3 | counter → 1, then sleep(50) | TIMED_WAITING | WAITING |
| 53 | timer fires, requeued; counter → 2, sleep | TIMED_WAITING | WAITING |
| 103 | counter → 3, sleep | TIMED_WAITING | WAITING |
| 153 | run() returns; JVM signals joiners | TERMINATED | RUNNABLE |
| 154 | main prints counter=3 TERMINATED | TERMINATED | RUNNABLE |
Two things to read off this: (1) main is WAITING, not BLOCKED — join() parks indefinitely on the target thread's exit, it is not contending for a lock. (2) Calling worker.start() twice would throw IllegalThreadStateException, because the NEW→RUNNABLE edge fires exactly once; there is no edge back to NEW.
The same logic in Go
Go has no thread life-cycle enum at all, because goroutines are not OS threads — they are user-space tasks the Go runtime multiplexes onto a small pool of OS threads (the G-M-P scheduler). You never inspect a goroutine's "state"; you express the dependency directly with a channel or a WaitGroup.
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var mu sync.Mutex
var wg sync.WaitGroup
counter := 0
wg.Add(1)
go func() { // "start()": runtime schedules the goroutine
defer wg.Done()
mu.Lock() // analogous to entering synchronized
defer mu.Unlock()
for i := 0; i < 3; i++ {
counter++
time.Sleep(50 * time.Millisecond) // goroutine parks; its OS thread is reused
}
}()
wg.Wait() // analogous to join(): main goroutine blocks here
fmt.Printf("counter=%d\n", counter)
}Where the runtimes differ:
- A Java thread maps 1:1 to an OS thread (~1 MB stack, kernel-scheduled). A goroutine starts with an ~8 KB growable stack and is scheduled in user space — you can run hundreds of thousands of them. The economic difference is the switch cost: parking/unparking a kernel thread costs on the order of ~1–3 µs — a syscall trap into the kernel, scheduler work to pick the next thread, and a TLB flush when the address space changes — whereas the Go runtime switches goroutines entirely in user space, saving a few registers and the stack pointer (tens to a couple hundred nanoseconds, no kernel entry). That ~10–100× gap is why 8 KB stacks plus user-space scheduling let you run 100k+ goroutines, where 100k OS threads would collapse the scheduler under switch overhead alone.
- When a Java thread calls
sleepor blocks on a lock, the OS thread is parked. When a goroutine sleeps or blocks on a channel, the Go runtime detaches it from its OS thread and runs another goroutine on that thread — the OS thread is not wasted. - Java exposes the state machine (
Thread.getState(), thread dumps showBLOCKED/WAITING). Go deliberately hides it; you reason about communication (channels) instead of state. A goroutine blocked on<-chis the rough analogue of a Java thread in WAITING.
Pitfalls
- Confusing BLOCKED with WAITING in a thread dump. BLOCKED means "parked on a monitor another thread owns" — if many threads are BLOCKED on the same lock, you have a contention hotspot. WAITING/TIMED_WAITING usually means the thread is correctly idle (pool worker waiting for a task). Misreading WAITING as a hang sends you chasing the wrong problem.
- Assuming RUNNABLE means "running." A thread doing blocking I/O on a socket often shows as RUNNABLE in a Java dump even though it is parked in the kernel, because the JVM can't tell the OS-level wait apart from CPU work. A box full of RUNNABLE threads at near-zero CPU is the classic signature of I/O wait, not CPU saturation.
- Calling
start()twice, or treating a TERMINATED thread as reusable. The life cycle is one-way; there is no edge back to NEW or RUNNABLE. To "reuse" a thread you must use a thread pool, which keeps a single long-lived thread RUNNABLE/WAITING and feeds it new tasks. - Using
Thread.getState()for control flow. The state can change the instant after you read it, so branching on it ("if WAITING then…") is inherently racy. State is a diagnostic, not a synchronization primitive — usejoin, locks, latches, or channels.
Takeaways
- A thread's state is just which queue the scheduler has it in; every transition is caused by a concrete call (
start,synchronized,sleep,wait,join) or a scheduler dispatch/preempt — never by the thread itself. - The JVM has no "Running" state: on-CPU and ready-to-run are both RUNNABLE. BLOCKED = waiting for a lock; WAITING/TIMED_WAITING = parked for a signal or deadline.
- The life cycle is one-directional and ends at TERMINATED — reuse means a thread pool, not restarting a thread.
- Go drops the state model entirely: goroutines are user-scheduled over a thread pool, so you reason about channels and communication instead of inspecting thread state.
Sources: Oracle Java SE documentation, java.lang.Thread.State enum and Thread javadoc; Brian Goetz et al., Java Concurrency in Practice (Ch. 7, thread lifecycle & cancellation); The Go Programming Language Specification and Go runtime scheduler design notes (G-M-P model); Donovan & Kernighan, The Go Programming Language (Ch. 8–9). Re-authored and deepened for this guide — replaced the definition-only state list and GIF with the scheduler/OS-queue mechanism, a step-by-step traced run with thread states, a hand-authored transition diagram, a side-by-side Go version, and real thread-dump pitfalls.
Reading a real jstack — BLOCKED vs WAITING vs RUNNABLE-on-I/O
Staff on-call skill: classify three look-alike "stuck" threads from a dump before guessing root cause.
"http-nio-8080-exec-14" #142 prio=5 os_prio=0 tid=0x00007f8a1c12b000 nid=0x6e3a
waiting for monitor entry [0x00007f89d4ffc000]
java.lang.Thread.State: BLOCKED (on object monitor)
at com.shop.Inventory.reserve(Inventory.java:88)
- waiting to lock <0x00000000eb441c10> (a java.lang.Object)
at com.shop.CheckoutService.placeOrder(CheckoutService.java:41)
at ...
"pool-2-thread-3" #55 prio=5 tid=0x00007f8a1c098800 nid=0x6a11
waiting on condition [0x00007f89d5ffd000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000000eb22aa00> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at java.util.concurrent.ThreadPoolExecutor.getTask(...)
"http-nio-8080-exec-7" #135 prio=5 tid=0x00007f8a1c110000 nid=0x6e11 runnable
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(...)
at okhttp3.internal.http1.Http1Codec.readHeaderLine(...)
at com.shop.PaymentClient.charge(PaymentClient.java:63)
| Snippet | State | What it actually means | Ops next step |
|---|---|---|---|
| exec-14 | BLOCKED | Wants monitor 0xeb441c10 held by someone else — lock contention / possible deadlock | jstack find who owns that monitor; check lock order |
| pool-2-thread-3 | WAITING (park) | Idle pool worker on empty queue — healthy idle, not a hang | Ignore unless all workers WAITING while latency high (then work never enqueued) |
| exec-7 | RUNNABLE | JVM thinks runnable, but stack is socketRead0 — blocked in kernel I/O | Trace remote payment p99; do not scale CPU for this |
When not to trust getState()
Never branch business logic on Thread.getState() — the answer is stale by the next instruction. Use it only as a diagnostic snapshot like the dump above.
Drill ladder
- L1: BLOCKED vs WAITING in one sentence each.
- L2: Why can RUNNABLE mean "waiting on network"?
- L3: Given 200 RUNNABLE threads at 5% CPU, what two hypotheses do you test first?
- L4: Write the jstack greps you'd run for a suspected lock-order deadlock.
🤖 Don't fully get this? Learn it with Claude
Stuck on Thread Life Cycle Within Concurrency? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Thread Life Cycle Within Concurrency** (Concurrency) and want to truly understand it. Explain Thread Life Cycle Within Concurrency from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Thread Life Cycle Within Concurrency** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Thread Life Cycle Within Concurrency** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Thread Life Cycle Within Concurrency** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.