AlgoInsight

Free lesson · Concurrency Interview Mastery

A thread is almost never just "running"

Easy10 min readconcurrencymultithreadinginterview-prep

Here's a thing that surprises people the first time they really look: at any given moment, most of the threads in a busy program are not running. They're waiting for something - a lock, some data, a timer, an I/O operation to finish. A thread spends much of its life in one waiting state or another, and being able to name which waiting state a thread is in is one of the most practical debugging skills in all of concurrency. When a program hangs, the question "what state is each thread stuck in?" is usually how you crack it.

So let's walk a thread through its whole life, from the moment you create it to the moment it dies, and name every state it can be in along the way.

The states, one at a time

Every threading system has roughly the same set of states. The names differ slightly by language - I'll use the Java names because they're the most explicit and the most commonly asked about, but the concepts are universal.

New. The thread object exists, but it hasn't started running yet. You've created it; you haven't pressed go. It's holding nothing and doing nothing - just an object waiting for the signal to begin.

Runnable. You've started the thread (in Java, you called start()), and now it's eligible to run. Note the word: eligible, not running. This state is the source of a subtlety people miss - "runnable" lumps together both "actually executing on a core right now" and "ready to execute, just waiting for the scheduler to give me a core." From the program's point of view, those are the same state: the thread is ready and willing; whether it's literally on a CPU this instant is up to the scheduler.

Running. A subset of runnable - the thread is actually executing on a core right now. Many systems don't distinguish this from runnable at the API level, but conceptually it's worth separating: runnable means "ready," running means "currently on a CPU." The scheduler is what moves threads between ready-and-waiting and actually-on-a-core, and it does so thousands of times a second.

Blocked. The thread wants to enter a section protected by a lock, but another thread holds that lock. So it's stuck at the door, waiting to acquire the lock before it can proceed. This is a specific kind of waiting - waiting for a monitor lock - and distinguishing it from the next state is a classic interview point.

Waiting. The thread is waiting indefinitely for another thread to do something - typically because it called something like wait() and is parked until another thread calls notify(). There's no timeout. It will sit there forever until explicitly woken. This is voluntary, cooperative waiting: the thread chose to step aside until signaled.

Timed Waiting. Same as waiting, but with a deadline. The thread called something like sleep(1000) or wait(500) - it'll wake up when notified or when the timer expires, whichever comes first. The presence of a timeout is the only difference from plain waiting.

Terminated. The thread has finished - either its work completed normally or it died from an unhandled exception. It's done. A terminated thread cannot be restarted; that's a one-way door.

stateDiagram-v2
    [*] --> New: thread created
    New --> Runnable: start()
    Runnable --> Blocked: wants a lock<br/>held by another
    Blocked --> Runnable: lock acquired
    Runnable --> Waiting: wait() /<br/>join() no timeout
    Waiting --> Runnable: notify() /<br/>notifyAll()
    Runnable --> TimedWaiting: sleep(t) /<br/>wait(t)
    TimedWaiting --> Runnable: timeout or notify
    Runnable --> Terminated: run() completes<br/>or throws
    Terminated --> [*]
This state diagram shows the lifecycle of a thread. A thread starts in the New state when created. Calling start moves it to Runnable. From Runnable it can move to several waiting states and back. It goes to Blocked when it wants a lock held by another thread, and returns to Runnable once it acquires the lock. It goes to Waiting when it calls wait or join with no timeout, and returns to Runnable when another thread calls notify. It goes to Timed Waiting when it sleeps or waits with a timeout, and returns to Runnable when the timeout expires or it is notified. Finally, from Runnable it moves to Terminated when its run method completes or throws an exception, and from there the thread is finished and cannot be restarted.

The distinction interviewers love: blocked vs. waiting

Of all of this, the one distinction that comes up again and again is blocked versus waiting, because they look identical from the outside - a thread that isn't making progress - but they mean different things and point to different problems.

A blocked thread is stuck trying to acquire a lock that another thread holds. It didn't choose to wait; it ran into a locked door. The fix to a blocked-thread problem is usually about lock contention or lock ordering - too many threads fighting over one lock, or a deadlock where everyone's blocked on each other.

A waiting thread chose to step aside - it called wait() (or join()) and is parked until another thread signals it. This is cooperative. The fix to a waiting-thread problem is usually about signaling - did someone forget to call notify()? Did the signal arrive before the thread started waiting (a lost wakeup)? Those are different bugs entirely.

Interview: "What's the difference between a blocked thread and a waiting thread?" is a frequent, sharp question. The answer that scores: "Blocked means the thread is stuck trying to acquire a lock that another thread holds - it's contending for a monitor. Waiting means the thread voluntarily parked itself by calling wait or join, and it'll stay parked until another thread notifies it. Both look stuck, but blocked points to lock contention or deadlock, while waiting points to a signaling problem - like a missing notify. When I debug a hung program, the first thing I do is take a thread dump and check which state each thread is in, because that immediately tells me which kind of problem I'm chasing."

That last sentence - connecting the states to thread dumps and real debugging - is what turns a textbook answer into a "this person has actually done it" answer.

Why this is a debugging superpower, not trivia

Let me make the practical payoff concrete, because this lesson can feel like vocabulary until you've used it in anger.

When a real program hangs in production, you take a thread dump - a snapshot of every thread and what state it's in. And the states tell you the story immediately:

Tip: The first move when any concurrent program misbehaves is: get a thread dump and read the states. You don't start by guessing or adding print statements everywhere. You look at where every thread actually is. Half of concurrency debugging is just reading the state of the world correctly before forming a theory. We dedicate a whole lesson to thread dumps later, but the habit starts here: when in doubt, ask what state each thread is in.

Reading a real thread dump

Let me turn the states from abstract vocabulary into something you can actually read, because a thread dump is where these state names stop being trivia and start solving your problem. A dump is just a snapshot listing every thread, its state, and what it was doing. A trimmed-down entry looks like this:

"worker-3" #21  BLOCKED
   waiting to lock <0x000000076ab2> (a com.app.Account)
   which is held by "worker-7"
   at com.app.Bank.transfer(Bank.java:42)

"worker-7" #25  BLOCKED
   waiting to lock <0x000000076cd4> (a com.app.Account)
   which is held by "worker-3"
   at com.app.Bank.transfer(Bank.java:42)

Read those two entries together and the bug announces itself. worker-3 is BLOCKED waiting for a lock held by worker-7, and worker-7 is BLOCKED waiting for a lock held by worker-3. Each is holding what the other needs. Neither can proceed. That's a deadlock, and you diagnosed it in about ten seconds purely by reading the states and who-holds-what - no print statements, no guessing. This is the payoff for knowing the states cold.

Contrast that with a dump where one thread sits alone in WAITING forever while every other thread has terminated - nobody is left to call notify(), so that's a lost-signal bug, a completely different fix. The state names are the diagnosis.

Tip: When you read a dump, scan the states column first, before the stack traces. The distribution tells you the category instantly: many BLOCKED in a cycle → deadlock; one lonely WAITING → missing signal; everything RUNNABLE but slow → CPU-bound or context-switch thrashing; many TIMED_WAITING → a downstream dependency is hanging. Only after you've categorized the problem from the states do you dive into the individual stack traces for the exact lines. State first, stack second.

One more distinction: daemon vs. user threads

There's a small but interview-worthy wrinkle in the lifecycle: threads come in two flavors. User threads keep the program alive - the process won't exit while any user thread is still running. Daemon threads are background helpers (think a logging flusher or a metrics reporter); the program will exit and abandon them the moment the last user thread finishes, even if a daemon is mid-task.

Warning: Don't put work you actually need to complete on a daemon thread. Because the JVM exits the instant the last user thread ends - without waiting for daemons - a daemon doing important cleanup or writing important data can be killed mid-operation with no warning. Daemons are for disposable background chores only. Getting this wrong leads to the maddening bug where data "sometimes" doesn't get flushed, depending on exactly when the main thread happens to finish.

Waking a thread up: interruption

There's one more transition worth knowing, because it answers a question that nags everyone the first time: if a thread is parked in Waiting or Timed Waiting, how do you ever get it out if you change your mind - say, the program is shutting down and you need stuck threads to stop?

The mechanism is interruption. One thread can signal another that it should stop what it's doing. If the target thread is parked in a waiting state, the interrupt wakes it - it pops out of Waiting back to Runnable, but instead of resuming normally it's handed an interruption to deal with (in Java, an InterruptedException). The thread then gets to decide how to respond: clean up and exit, or ignore it and keep going.

The key thing to understand is that interruption is cooperative, not forceful. You are not killing the thread. You are politely tapping it on the shoulder and saying "please consider stopping." A thread that's busy in a tight computation and never checks its interrupt flag, and never hits a blocking call, will sail right past the request. Interruption is a request the target has to honor, not a command the OS enforces.

Warning: There is no safe way to forcibly kill a running thread, and the old methods that tried (Java's Thread.stop()) are deprecated for a brutal reason: a force-killed thread could die holding a lock, leaving that lock permanently locked and every other thread blocked on it forever - or die halfway through updating shared state, leaving it corrupted. So the only sane shutdown model is cooperative: signal the thread, and have the thread check for the signal at safe points and bow out cleanly on its own. "How do you stop a thread?" is a real interview question, and "you don't force it - you request it via interruption and the thread cooperates" is the answer that shows you understand why forcing is dangerous.

What moves a thread between states

One more layer, because it ties the room together: threads don't change their own states arbitrarily - specific events trigger specific transitions. start() moves New to Runnable. The scheduler shuffles threads between ready and actually-on-a-core. Trying to enter a held lock moves Runnable to Blocked. Calling wait() or join() moves it to Waiting. sleep() or a timed wait moves it to Timed Waiting. A notify() from another thread, or a timeout, moves it back toward Runnable. And the run() method finishing - or throwing - moves it to Terminated.

Warning: A subtle and painful one: a thread can be sitting in Waiting forever if the notify() that was supposed to wake it already happened before it started waiting. The signal fires into the void, then the thread parks itself and waits for a notification that already came and went. This is the lost wakeup problem, and it's why real code waits inside a loop that re-checks a condition rather than waiting blindly. We'll dissect this fully in the challenges module - for now, just register that "waiting forever" is sometimes caused by a signal that arrived too early, not a signal that never came.

Note: Different languages name these states slightly differently - Java is the most explicit with its six-state enum, while others collapse a few - but every threading system has the same conceptual shape: not-yet-started, ready-or-running, one or more flavors of waiting, and done. Learn the shape and the names translate easily.

What you've got now

You can now look at any non-progressing thread and ask the right question - not "why isn't it running?" but "which state is it in, and what does that state tell me?" New means not started. Runnable means ready or running. Blocked means stuck on a lock. Waiting means parked for a signal with no deadline. Timed waiting means parked with a deadline. Terminated means done for good. And the blocked-vs-waiting distinction, in particular, instantly narrows a hung program down to "lock problem" versus "signaling problem."

That closes out the building blocks of Module 1's first half - what concurrency is, how it differs from parallelism, the process-vs-thread machinery, and the states a thread lives through. In the next batch we put these pieces in motion and watch them collide: race conditions and critical sections, where understanding the interleaving model from lesson one finally pays off in full - and where the course's animations do their most important work.

Quiz: A thread dump shows Thread X in the BLOCKED state and Thread Y in the WAITING state. What does each most likely indicate? A) Both are deadlocked B) X is stuck trying to acquire a lock held by another thread; Y voluntarily parked itself and is waiting to be notified C) X is sleeping on a timer; Y has terminated D) Both are actively running and just slow

Answer: B. BLOCKED means contending for a lock another thread holds (a contention or deadlock smell). WAITING means the thread called something like wait or join and is parked until notified (a signaling smell). Different states, different classes of bug.

Back to Concurrency Interview Mastery