Textbooks draw a thread lifecycle as five boxes: created, runnable, running, blocked, terminated. It is a fine first picture and a poor tool for debugging, because no real runtime reports those states. Java reports six states that do not include running. Linux reports one-letter task states that do not distinguish waiting on a lock from waiting on a disk. A thread that a Java thread dump calls RUNNABLE may be asleep in the kernel waiting for a socket, and a thread the operating system calls running may be a Java thread spinning on a lock.

This page maps the real models onto each other, so that the state you read in a thread dump, in top or in a debugger tells you what the thread is actually doing. It covers how a thread is born and what it costs, every way it can wait and what wakes it, how it dies, what must happen after it dies, and how to read a stuck process from the distribution of its thread states. Java facts are from the Java 21 Thread documentation; the Linux and POSIX behavior is standard.

Three layers, three sets of states

Three layers each keep their own state for the same thread. The language runtime tracks what the thread is doing in terms of the language: waiting for a monitor, parked, sleeping. The operating system tracks whether the kernel task is on a CPU, ready to run, or asleep, and if asleep whether a signal can wake it. The threading library, such as POSIX threads, tracks bookkeeping: whether anyone will collect the thread's result, and whether its stack can be freed. None of these layers sees the others completely, so each state report is true about its own layer only.

java.lang.Thread.State: six states, and the events that move a thread between themNEWconstructedstart()RUNNABLEon CPU, ready, or in native I/Orun() returns or throwsTERMINATEDjoin() returnsBLOCKEDwaiting to enter a monitorsynchronized, heldmonitor acquiredWAITINGwait(), join(), park()no timeoutnotify/unparkTIMED_WAITINGsleep(ms), wait(ms), parkNanoswith timeouttimeout or wakewoken from wait(): re-enter monitorRUNNABLE has no running/ready split: the JVM does not know whether the OS has the thread on a CPU
Java's thread state machine. A thread woken from Object.wait passes through BLOCKED because it must reacquire the monitor before it continues.

The six Java thread states

StateHow a thread gets thereWhat moves it on
NEWThread object constructed, start() not calledstart()
RUNNABLEExecuting Java code, ready to run, or inside a native call such as a blocking socket readLock contention, waits, completion
BLOCKEDEntering a synchronized block or method whose monitor another thread holds; or returning from wait()The monitor is released and acquired
WAITINGObject.wait(), Thread.join() or LockSupport.park() with no timeoutnotify, the joined thread ending, unpark, or interrupt
TIMED_WAITINGThread.sleep, or wait, join or parkNanos with a timeoutTimeout, wake-up or interrupt
TERMINATEDrun() returned or threwNothing; a thread cannot be restarted

Two consequences confuse almost everyone the first time. First, java.util.concurrent locks such as ReentrantLock do not produce BLOCKED. Their waiters park, so a thread stuck behind a contended ReentrantLock shows WAITING, with LockSupport.park on its stack. Only synchronized monitors produce BLOCKED. Second, RUNNABLE is not proof of work. A platform thread blocked in a socket read on a quiet connection is RUNNABLE in the dump, because from the JVM's point of view it is executing a native method. You have to read the top stack frame, not just the state.

The operating system view

On Linux each Java platform thread is a kernel task, and each has its own entry under /proc/<pid>/task/<tid>. The kernel's view is coarser and lower level. R means running or ready to run: on a CPU or in a run queue. S is interruptible sleep: waiting for an event, such as a futex wake, a timer or data on a socket, and wakeable by signals. D is uninterruptible sleep, usually waiting on disk or network file system I/O, and counts towards the load average even though it uses no CPU. T is stopped, by a signal or a debugger. Z is a zombie: an exited task whose parent has not yet collected it.

Mapping the two layers explains most mixed signals. A Java thread in BLOCKED, WAITING or TIMED_WAITING is normally S at the OS level, sleeping on a futex. A RUNNABLE Java thread may be R, if it is executing code, or S, if it is in a blocking native call. A thread showing D for long periods points at storage, not at your locks. The OS run queue and how threads are picked from it are covered in thread scheduler architecture.

# Count the kernel state of every thread in a JVM process.
# The state is the first field after the ")" that closes the thread name in stat;
# thread names can contain spaces, so do not split stat on whitespace from the start.
pid=$(pgrep -f my-service.jar)
for t in /proc/$pid/task/*; do
  sed 's/.*) //' "$t/stat" | cut -d' ' -f1
done | sort | uniq -c
# A jstack line's nid=0x... is the kernel thread id (the task directory name) in hex

Birth: start() and what it costs

A Java thread is born twice. Constructing the object creates NEW state and nothing else; no kernel resource exists yet. Calling start() asks the JVM to create a native thread, which on Linux means allocating a stack (its maximum size set by -Xss or the constructor's stack-size argument), calling into the C library, and from there the clone system call. Only then does the new thread begin executing run(). A thread can be started at most once: a second start() throws IllegalThreadStateException, even after the thread has terminated. Python behaves the same way, raising RuntimeError when a Thread object is started twice.

Creation is cheap compared with a network call but not free: a kernel task, a stack mapping, and thread-local structures in the runtime. That cost, and the memory held by each idle thread, is why servers reuse threads through pools rather than creating one per task; see thread pools in depth. Virtual threads change the arithmetic, covered below.

Waiting and waking

Waiting is where threads spend most of their lives, and the exit from each wait has a specific trigger. A thread in a monitor wait must be notified and then reacquire the monitor, which is why the predicate must be rechecked in a loop: by the time it runs again, the condition may have changed. A thread parked by a lock is unparked by the releasing thread. A sleeping thread wakes when the timer expires. And interrupt is the general escape hatch: interrupting a thread that is in wait, join or sleep makes that call throw InterruptedException, while a parked thread simply returns with its interrupt flag set.

public class Lifecycle {
    public static void main(String[] args) throws Exception {
        Object lock = new Object();
        Thread t = new Thread(() -> {
            synchronized (lock) {
                try { lock.wait(); } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();   // keep the flag for callers
                }
            }
        }, "worker");

        System.out.println(t.getState());   // NEW
        t.start();
        Thread.sleep(100);
        System.out.println(t.getState());   // WAITING (inside lock.wait())

        synchronized (lock) {
            lock.notify();
            Thread.sleep(100);
            System.out.println(t.getState()); // BLOCKED: notified, needs the monitor we still hold
        }
        t.join();
        System.out.println(t.getState());   // TERMINATED
    }
}

The sleeps make this a demonstration rather than a test, since thread timing is never guaranteed, but on an idle machine it prints the four states in order. Condition waits and the Mesa semantics behind the recheck loop are covered in condition variable architecture.

Death, cancellation and collection

A thread ends when run() returns or throws. An uncaught exception is passed to the thread's uncaught exception handler, or its group's, or the default handler, which by default prints the stack trace and lets the thread die; the rest of the process keeps running. In a pool this means a task's exception can silently kill a worker unless the pool catches it, which is why submitting to an executor wraps the exception in the returned future instead.

There is no safe way to kill a thread from outside. Thread.stop was deprecated for decades because it threw an exception at an arbitrary point in the victim, possibly half way through updating shared state; in current JDKs, including JDK 21, it simply throws UnsupportedOperationException. Cancellation is cooperative: set a flag or interrupt the thread, and write the thread to check for it at safe points. Blocking calls that do not respond to interrupt, such as some legacy socket reads, need the resource closed to unblock them.

After death comes collection. In POSIX threads a thread is joinable by default: when it exits, the kernel task goes away but the C library keeps its stack and return value until another thread calls pthread_join. A program that creates joinable threads and never joins or detaches them leaks that memory per thread until creation fails. Either join every joinable thread or create it detached. In Java the JVM handles this, and join() is purely for waiting. Process exit has its own rules: the JVM's shutdown sequence begins when all started non-daemon threads have terminated, so a forgotten non-daemon thread keeps a process alive, and daemon threads are abandoned mid-work at exit. In C, returning from main ends the process and every thread in it, while calling pthread_exit in main lets the others continue.

Virtual threads

Virtual threads keep the same six states but separate the Java thread from the kernel task. A virtual thread runs by mounting on a carrier platform thread; when it blocks in a supported operation, it unmounts, its stack is saved to the heap, and the carrier runs another virtual thread. So a virtual thread in WAITING costs memory, not a kernel task, and a blocking socket read no longer pins a platform thread as RUNNABLE. Virtual threads are always daemon threads, so they never hold the JVM open at shutdown. Pinning, where a virtual thread cannot unmount, and the scheduler behind mounting are covered in JVM virtual threads architecture.

Worked example: a hung service with idle CPUs

A service stops answering requests although its process is up and CPU is near zero. A thread dump shows 200 request threads. Counting by state: 4 RUNNABLE, 190 WAITING and 6 TIMED_WAITING. The 4 RUNNABLE threads are the acceptor and three threads in a blocking socket read, each talking to the same downstream payment service. The 190 WAITING threads all have LockSupport.park under a connection pool's borrow method.

Read with the mapping above, the story is clear. A WAITING-heavy dump could hide a lock deadlock, but jstack reports none (its detector covers monitors and java.util.concurrent locks), and every parked thread is queued on the same pool borrow rather than on locks held by each other. Nothing is burning CPU either. The pool has 3 connections, all held by threads stuck in reads with no socket timeout, and every other request thread is parked waiting to borrow one. At the OS level all 200 threads would show S. The fix is a read timeout on the downstream client, a borrow timeout on the pool so request threads fail instead of waiting forever, and a pool sized for the real concurrency. A real deadlock would show threads each holding a lock another is waiting for; locks and mutexes covers that case.

Failure modes

  • Reading RUNNABLE as busy. Threads in native blocking I/O show RUNNABLE. Check the top frame and OS state before concluding a CPU problem.
  • Expecting BLOCKED for lock contention. java.util.concurrent locks park their waiters, so contention shows as WAITING in park. Search stacks, not just states.
  • Swallowed thread death. An uncaught exception kills a hand-made worker thread and nothing notices. Install an uncaught exception handler that logs and alerts.
  • Leaked joinable threads. pthreads that are never joined or detached keep their stacks. Detach or join every thread.
  • Process that will not exit. A forgotten non-daemon thread holds the JVM open. Name threads and mark background helpers as daemon deliberately.
  • Uncancellable work. A thread ignores interrupts and blocks shutdown. Check the interrupt flag in loops and use timeouts on blocking calls.

Trade-offs

ChoiceGainCost
New platform thread per taskSimple isolationCreation cost and memory per thread; no bound on count
Pooled platform threadsReuse, bounded concurrencyPool sizing; queued tasks; exception handling
Virtual threadsBlocking code at high concurrency; cheap waitingPinning cases; still need limits on downstream resources
Daemon threadNever blocks shutdownKilled mid-work at exit
Non-daemon threadWork finishes before exitCan hold the process open

What to do next

  1. Name every thread you create; unnamed threads make dumps unreadable.
  2. Take a thread dump of a healthy service and record the normal count per state and per top frame, so you can recognise abnormal ones.
  3. Install a default uncaught exception handler that logs and increments a metric.
  4. Audit blocking calls for timeouts: socket reads, pool borrows, lock acquisitions with tryLock.
  5. Make long-running loops check for interruption and exit cleanly.
  6. In C or C++, ensure every pthread is joined or detached.
  7. When something hangs, count threads by state and by top frame, then map to OS state with /proc before changing any code.
Key takeaway: A thread has a separate state in each layer: Java reports NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED, Linux reports running, sleeping, uninterruptible, stopped and zombie, and POSIX threads track whether a dead thread has been joined. RUNNABLE in Java includes threads blocked in native I/O, and only synchronized monitors produce BLOCKED. A thread starts once, is cancelled only cooperatively, and must be joined or detached in C. Diagnose hangs by counting threads per state and per top stack frame, then checking the kernel state.