Walk through the states a thread passes through from creation to termination, and explain the difference between a thread blocked trying to acquire a lock and a thread waiting to be signalled.
answer
- new → runnable → (blocked | waiting | timed-waiting) → terminated
- runnable merges ready + running
- blocked = contention, involuntary, wakes on lock release
- waiting = coordination, voluntary, needs a signal
- terminated is absorbing — no restart, pools reuse workers not states
basics
~20 sNew (created, not started) → runnable (ready or actually running) → a non-runnable state → terminated, which is final. Blocked means it wants a lock someone else holds. Waiting means it voluntarily parked and needs another thread to signal it; timed-waiting also wakes on a deadline.
solid answer
~50 sA typical lifecycle model has five or six states: - **New** — the thread object exists, no execution has begun. - **Runnable** — eligible to run; the scheduler may or may not have it on a CPU right now. Most models deliberately do not separate "ready" from "running", because that distinction changes on every timeslice. - **Blocked** — stalled acquiring a mutual-exclusion lock another thread holds. The thread did not choose to stop; it will resume automatically when the lock frees. - **Waiting** — parked voluntarily on a condition, another thread's termination, or a signal. It resumes only when someone else signals/notifies it. - **Timed-waiting** — as waiting, but with a deadline that also wakes it. - **Terminated** — the entry function returned or threw out. Absorbing state: a terminated thread can never be restarted. The distinction that matters: **blocked is contention, waiting is coordination**. High blocked counts mean a hot lock; high waiting counts usually mean idle workers or a slow dependency.
code
text · 11 linesstart() lock free
new ---------> runnable <----------------- blocked
| ^ ^
await/join | | signal + re-acquire | lock held by other
v | |
waiting ------------------------
|
deadline set | entry fn returns/throws
v |
timed-waiting v
terminated (absorbing)go deeper
Recite the states in order and give one concrete cause for each; be clear that terminated is final.
Nail blocked-versus-waiting by cause and wake condition, and mention that runnable deliberately merges ready and running.
Turn the states into a diagnostic method: what a dump full of blocked threads means versus one full of waiting threads, and what you change in each case.
Treat the model as an approximation sampled without stopping the world, discuss its limits under lightweight/virtual-thread schedulers, and argue for capacity decisions driven by state distributions rather than single dumps.
## Why a state model exists at all A thread is a flow of control plus the bookkeeping a scheduler needs: a stack, a register set, and a *state*. The state answers one question — is this thread eligible to be put on a CPU right now, and if not, what event would make it eligible? Every runtime exposes some variation of the same five or six states, because those are the answers that exist. ## The states **New / created.** The thread's bookkeeping exists but no stack has begun executing. Nothing runs yet. In most runtimes this is the only state from which a start operation is legal, and starting twice is an error. **Runnable.** The thread can execute. Crucially, this usually *merges* two operating-system-level conditions: *ready* (in the scheduler's run queue, waiting for a core) and *running* (currently on a core). Runtimes merge them because the distinction flips thousands of times a second and any observation would be stale before you read it. A thread performing a blocking system call — a disk read, a socket receive — is often still reported as runnable even though the OS has descheduled it, because the runtime does not track kernel-internal states. **Blocked.** The thread attempted to enter a mutual-exclusion region and another thread holds the lock. This is *involuntary*: the thread did not ask to stop, it was stopped by contention, and it needs no signal to continue — when the owner releases the lock, one of the blocked contenders is made runnable again. **Waiting.** The thread called something that says "suspend me until told otherwise": awaiting a condition variable, joining another thread, or parking on a synchronizer. This is *voluntary* and *unbounded*. Nothing but an explicit signal, notification, target-thread termination, or an interrupt-style cancellation request will wake it. If the signal never comes, the thread waits forever — a lost-wakeup bug. **Timed waiting.** Identical to waiting except a deadline is registered, so the thread wakes on signal *or* on expiry. Sleep, timed condition-waits, and timed joins land here. Timed variants are strictly safer in production because a lost signal degrades to a delay instead of a permanent hang. **Terminated.** The entry function returned normally, or an exception escaped it. The thread's stack is gone. This state is *absorbing*: there is no restart. Reusing a "thread" therefore always means reusing a pooled worker whose entry function loops over tasks — the underlying thread never re-enters new. ## Blocked vs waiting — the interview crux They look identical from outside (both consume no CPU, both show as a stalled stack), so name the three differences: 1. **Cause.** Blocked = contention on a lock. Waiting = an explicit coordination call. 2. **Wake condition.** Blocked wakes when the lock becomes free, with no cooperation from anyone in particular. Waiting wakes only when a specific party signals, the deadline expires, or cancellation arrives. 3. **What it tells you when you see it.** A thread dump with dozens of threads blocked on one monitor identifies a serialization bottleneck: your critical section is too coarse or too slow, and throughput is capped by it. Dozens of threads waiting on a work queue is usually *healthy* — those are idle pool workers. Dozens waiting on a condition that no one signals is a lost-wakeup or a deadlock in disguise. A nuance interviewers like: a thread that *wakes* from a condition wait must re-acquire the associated lock before it can return, so it typically passes waiting → blocked → runnable. That is why a burst of notifications produces a thundering herd of blocked threads. ## Transitions worth naming ``` new --start--> runnable <--descheduled/scheduled--> (running) runnable --lock held by other--> blocked --lock released--> runnable runnable --await/join/park--> waiting --signal/target ends/cancel--> (re-acquire lock) --> runnable runnable --sleep/timed await--> timed-waiting --signal or deadline--> runnable runnable --entry fn returns or throws--> terminated [absorbing] ``` ## Why the model is only an approximation These are *runtime-level* states, not the OS scheduler's internal states, and they are sampled without stopping the world, so a dump is a slightly smeared picture. Two consequences for practice: (a) never write logic that branches on another thread's observed state — by the time you read it the state may have changed, which makes such checks inherently racy; (b) do use *repeated* samples for diagnosis, because the aggregate distribution across many threads is stable enough to be meaningful even when any single reading is not. Also note that on runtimes with lightweight/virtual threads scheduled in user space, a "blocking" operation may unmount the thread from its carrier rather than block a kernel thread; the reported state is about the logical thread, and the CPU-level cost of that stall can be radically lower. ## How to use this in practice Take several thread dumps a few seconds apart. Threads consistently *blocked* on the same lock → reduce the critical section, shard the lock, or replace it with an immutable/lock-free structure. Threads consistently *waiting* on the same external call → that dependency is your latency floor, and you should add timeouts and bulkheads. Threads consistently *runnable* while CPU is pinned → you are compute-bound and more threads will only add context switches.
- When a waiting thread is signalled, does it immediately become runnable?Not immediately in the common case. A condition wait is tied to a lock, and a woken thread must re-acquire that lock before it can return from the wait, so it typically moves waiting → blocked → runnable. That is why broadcasting to many waiters produces a thundering herd: they all wake, all contend for one lock, and all but one go straight back to blocked.
- You take a thread dump and see forty threads in the waiting state. Is that a problem?By itself, no — idle pool workers parked on an empty task queue are waiting, and that is the healthy steady state. It becomes a problem when the thing they wait for should have arrived: waiters on a condition that nothing signals is a lost wakeup, and waiters on a response from a dependency mean that dependency is your latency floor. Diagnose by comparing successive dumps and looking at what each waiter is parked on, not at the count alone.
- Why can a terminated thread never be restarted, and how do thread pools appear to get around it?Termination is absorbing because the thread's stack and execution context are gone; the identity that remains is just a handle. A pool never restarts terminated threads — its workers run a loop that takes a task, runs it, and takes the next, so the underlying thread stays alive across many tasks. That is also why per-task state left on a pooled thread leaks into later tasks.
Blocked is queuing at an occupied restroom door — nobody has to invite you in, you just go when it opens. Waiting is sitting in a doctor's waiting room — you stay seated indefinitely until someone calls your name; timed-waiting is agreeing to leave if nobody calls you within an hour.
saying these in an interview costs you the question
- Claiming there is a separate 'running' state you can observe reliably — runnable merges ready and running for good reason
- Using blocked and waiting as synonyms, so a lock bottleneck and an idle pool look the same in diagnosis
- Thinking a thread blocked on a socket read is 'blocked' in the runtime's sense — it is usually still reported as runnable
- Believing a terminated thread can be started again
- Branching program logic on another thread's observed state, which is racy by construction