Last Updated: January 30, 2026
A thread doesn't just run from start to finish in one continuous burst. It moves through different states: waiting to start, actively running, blocked on I/O, waiting for a lock, and eventually terminating.
When your multi-threaded application hangs or behaves unexpectedly, understanding these states is often the key to diagnosing what went wrong.
A thread's lifecycle is the sequence of states it passes through from creation to termination. Think of it like the lifecycle of an employee at a company: hired (created), onboarding (ready), actively working (running), waiting for resources or approvals (blocked/waiting), and eventually leaving the company (terminated).
At any given moment, a thread exists in exactly one state. External events and method calls cause transitions between states. The operating system's scheduler decides which runnable threads actually get CPU time.
At the operating system level, threads have these fundamental states:
| State | Description |
|---|---|
| Ready | Thread can run, waiting for CPU time |
| Running | Actively executing on a CPU core |
| Blocked | Waiting for I/O, lock, or external event |
| Terminated | Finished execution, cannot run again |
Every language's thread states map to these OS-level concepts, but the granularity of exposure varies dramatically. The diagram below shows the complete state model that we'll reference throughout this chapter:
Before diving into each state, it's important to understand that languages take fundamentally different approaches to exposing thread states. This isn't just an implementation detail, it reflects different design philosophies.
Java provides Thread.State, an enum with six values: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. You can query any thread's state at any time with thread.getState().
This makes Java excellent for debugging concurrency issues. You can programmatically detect deadlocks by finding threads in BLOCKED state waiting for each other's locks.
C#'s ThreadState is a flags enum, meaning a thread can be in multiple states simultaneously. For example, a background thread that's sleeping might have ThreadState.WaitSleepJoin | ThreadState.Background. This is more flexible but requires bitwise operations to check states properly.
Python's threading module only tells you if a thread is_alive(). C++ only tells you if a thread is joinable() (started but not yet joined). For detailed state analysis, you need external tools or platform-specific APIs.
Go takes the most radical approach: goroutine states simply aren't exposed. There's no getState() method, no state enum, nothing. This is intentional.
Go's philosophy is that if you need to check a goroutine's state, you're probably doing something wrong. Instead of inspecting state, you should:
sync.WaitGroup to wait for goroutinescontext.Context for cancellationThis design pushes you toward patterns that are inherently safer and more composable.
Now let's examine each state in detail, with code examples showing how to observe or trigger that state in each language.
A thread in the NEW state has been created as an object in memory, but hasn't started executing yet. The operating system hasn't allocated resources for its execution.
In this state:
Once start() is called, the thread moves to the RUNNABLE state. The OS has created the thread and it's eligible to run, but it might not be running right now. The scheduler decides which runnable threads get CPU time.
In this state:
A thread is RUNNING when it's actively executing instructions on a CPU core. This is a sub-state of RUNNABLE in most models. The thread has been selected by the scheduler and is consuming CPU cycles.
A thread leaves the RUNNING state when:
The scheduler continuously moves threads between RUNNABLE and RUNNING. On a 4-core machine, at most 4 threads can be RUNNING simultaneously. Others wait in the runnable queue.
In this example:
The distinction between "ready to run" and "actually running" exists at the OS level, but user-space programs typically can't observe it reliably.
By the time you query a thread's state and receive the answer, the thread may have been preempted or resumed multiple times. Java, C#, Python, and Go all combine these into a single "alive and not blocked" concept.
A thread enters the BLOCKED state when it tries to acquire a lock (monitor) that another thread holds. It cannot proceed until the lock becomes available.
This is different from WAITING. BLOCKED specifically means "waiting to enter a synchronized block or method." The thread isn't waiting for a signal; it's waiting for exclusive access to a resource.
A thread enters the WAITING state when it explicitly waits for another thread to perform an action. Unlike BLOCKED, the thread isn't competing for a lock; it's parked until signaled.
Common triggers for WAITING:
Object.wait(), Thread.join(), LockSupport.park()Monitor.Wait(), Thread.Join(), ManualResetEvent.WaitOne()Condition.wait(), Thread.join(), Event.wait()condition_variable.wait(), thread.join()sync.WaitGroup.Wait()The thread will stay in WAITING forever unless another thread wakes it up.
TIMED_WAITING is similar to WAITING, but with a timeout. The thread will wake up either when signaled or when the timeout expires, whichever comes first.
Common triggers:
Thread.sleep(millis), Object.wait(millis), Thread.join(millis)Thread.Sleep(millis), Monitor.Wait(obj, millis), Thread.Join(millis)time.sleep(secs), Condition.wait(timeout), Thread.join(timeout)sleep_for(), wait_for(), join() doesn't have native timeouttime.Sleep(), select with timeout, context.WithTimeout()A thread enters the TERMINATED state when its execution completes, either normally or by throwing an uncaught exception. The thread can never run again. Its resources are released.
In this state:
isAlive() returns falsestart() again throws an exception| From State | To State | Trigger |
|---|---|---|
| NEW | RUNNABLE | start() / Start() called |
| RUNNABLE | RUNNING | Scheduler assigns CPU |
| RUNNING | RUNNABLE | Time slice expires, yield() |
| RUNNING | BLOCKED | Attempt to acquire held lock |
| RUNNING | WAITING | Indefinite wait call (wait(), join(), channel receive) |
| RUNNING | TIMED_WAITING | Bounded wait call (sleep(), wait(timeout)) |
| RUNNING | TERMINATED | Execution completes or exception |
| BLOCKED | RUNNABLE | Lock acquired |
| WAITING | RUNNABLE | Signaled (notify(), channel send, Pulse()) |
| TIMED_WAITING | RUNNABLE | Timeout expires or signaled |