CMD Guide
HomeConcurrencyConcurrency Foundations

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:

StateWhat the thread is doingHow it got here
NEWObject exists; no OS thread yetnew Thread(r) — nothing scheduled
RUNNABLEOn the ready queue or executing on a corestart() creates the OS thread and enqueues it
BLOCKEDParked at a monitor lock it could not acquireHit synchronized while another thread owns the monitor
WAITINGParked indefinitely until signalledwait(), join(), LockSupport.park()
TIMED_WAITINGParked until signalled or a deadlinesleep(ms), wait(ms), poll(timeout)
TERMINATEDrun() returned or threw; OS thread reclaimedExit 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.

diagram
diagram

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)EventT-worker statemain state
0Object constructed, before start()NEWRUNNABLE
1worker.start() → OS thread enqueuedRUNNABLERUNNABLE
2main calls worker.join()RUNNABLEWAITING
3worker enters synchronized (lock free)RUNNABLEWAITING
3counter → 1, then sleep(50)TIMED_WAITINGWAITING
53timer fires, requeued; counter → 2, sleepTIMED_WAITINGWAITING
103counter → 3, sleepTIMED_WAITINGWAITING
153run() returns; JVM signals joinersTERMINATEDRUNNABLE
154main prints counter=3 TERMINATEDTERMINATEDRUNNABLE

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:

Pitfalls

Takeaways


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)
SnippetStateWhat it actually meansOps next step
exec-14BLOCKEDWants monitor 0xeb441c10 held by someone else — lock contention / possible deadlockjstack find who owns that monitor; check lock order
pool-2-thread-3WAITING (park)Idle pool worker on empty queue — healthy idle, not a hangIgnore unless all workers WAITING while latency high (then work never enqueued)
exec-7RUNNABLEJVM thinks runnable, but stack is socketRead0 — blocked in kernel I/OTrace 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

🤖 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.

🎨 Explain it visually

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.
🤔 Walk me through it (interactive)

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.
🧪 Quiz me & fix my gaps

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.
🧠 Make it stick

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.

📝 My notes