Thread Lifecycle

Every Java thread moves through a well-defined set of states — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and knowing the legal transitions between them is essential for debugging concurrency issues.

The JVM tracks every Thread object's state as one of six values defined by the Thread.State enum. These states describe what the JVM knows about a thread's scheduling situation, not what the underlying operating system is doing with it at that exact instant. This topic walks through each state, the events that move a thread between them, and the important distinction between Java's notion of thread state and actual OS-level CPU scheduling, which the JVM does not control or expose.

The Thread.State Enum

java.lang.Thread.State is a nested enum with exactly six constants: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. Thread.getState() returns the current value for a given Thread object at the moment it's called.

A freshly constructed Thread reports NEW until start() is called.

Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW

NEW: Created but Not Started

A thread is in the NEW state from the moment its Thread object is constructed until start() is invoked on it. In this state the thread has a Thread object but no corresponding OS thread has been created yet — nothing is scheduled and no code inside its run() method has executed.

A Thread object can be started at most once. Calling start() a second time on the same Thread object throws IllegalThreadStateException — there is no way to move a TERMINATED thread back to NEW or re-run it.

RUNNABLE: Executing or Eligible to Execute

Once start() is called, the thread becomes RUNNABLE. This state is somewhat misleadingly named: it does not mean the thread is actually running on a CPU right now — it means the thread is eligible to run and is either currently executing, or waiting for the OS scheduler to give it a CPU time slice. Java's RUNNABLE state deliberately does not distinguish 'actually running' from 'ready and waiting for a core,' because that distinction belongs to the operating system's scheduler, not the JVM.

Immediately after start(), the thread is almost certainly RUNNABLE — either running the loop or queued to run it.

Thread t = new Thread(() -> {
    long sum = 0;
    for (long i = 0; i < 1_000_000_000L; i++) sum += i;
});
t.start();
System.out.println(t.getState()); // very likely RUNNABLE

BLOCKED: Waiting to Acquire a Monitor Lock

A thread enters BLOCKED specifically when it is trying to enter a synchronized method or block but another thread currently holds that object's intrinsic lock. BLOCKED is exclusively about contention for an intrinsic (synchronized) monitor — it is not used for java.util.concurrent locks like ReentrantLock, which produce WAITING/TIMED_WAITING instead.

contender is blocked on the same monitor holder is sleeping while holding — it cannot proceed until holder exits the synchronized block.

Object lock = new Object();

Thread holder = new Thread(() -> {
    synchronized (lock) {
        try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
    }
});
Thread contender = new Thread(() -> {
    synchronized (lock) { System.out.println("got it"); }
});
holder.start();
holder.getState();      // observe briefly, then...
contender.start();
Thread.sleep(100);
System.out.println(contender.getState()); // BLOCKED, waiting for lock

WAITING: Indefinitely Suspended by Choice

A thread is WAITING when it has voluntarily suspended itself with no timeout, and will remain suspended until another thread performs a specific action to wake it. The classic triggers are Object.wait() (with no timeout), Thread.join() (with no timeout), and LockSupport.park().

join() with no arguments blocks joiner until worker finishes, and joiner reports WAITING while it does.

Thread worker = new Thread(() -> {
    try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
});
worker.start();

Thread joiner = new Thread(() -> {
    try { worker.join(); } catch (InterruptedException ignored) {} // waits with no timeout
});
joiner.start();
Thread.sleep(100);
System.out.println(joiner.getState()); // WAITING

TIMED_WAITING: Suspended With a Timeout

TIMED_WAITING is the same idea as WAITING, but bounded by an explicit timeout — the thread will wake up on its own once the timeout elapses, even if nothing else notifies or interrupts it. Common triggers: Thread.sleep(millis), Object.wait(timeout), Thread.join(timeout), and LockSupport.parkNanos/parkUntil.

Thread.sleep(3000) puts the thread into TIMED_WAITING for up to three seconds, or until interrupted.

Thread t = new Thread(() -> {
    try { Thread.sleep(3000); } catch (InterruptedException ignored) {}
});
t.start();
Thread.sleep(100);
System.out.println(t.getState()); // TIMED_WAITING

TERMINATED: Finished Execution

A thread reaches TERMINATED once its run() method has completed — either by returning normally or by an uncaught exception propagating out of it. A TERMINATED thread is done permanently: it holds no more CPU resources, and calling start() on it again throws IllegalThreadStateException. isAlive() returns false once a thread is TERMINATED (and also before it starts, so isAlive() alone cannot distinguish NEW from TERMINATED).

join() here waits for t to finish before checking its state, guaranteeing TERMINATED is observed.

Thread t = new Thread(() -> {});
t.start();
t.join();
System.out.println(t.getState());  // TERMINATED
System.out.println(t.isAlive());   // false

State Transition Diagram

All transitions ultimately flow toward TERMINATED; RUNNABLE is the hub state every other state returns to before a thread can finish.

start()                     run() returns / throws
  NEW ----------> RUNNABLE ---------------------------------> TERMINATED
                    ^  |  ^
     lock acquired   \ |  | notify()/notifyAll(), join target
     (monitor free)   \|  | finishes, unpark(), interrupt()
                       v  |
                   BLOCKED (waiting for a synchronized monitor)
                       ^
                       | contends for a monitor
                       |
             WAITING <----+----> TIMED_WAITING
        (wait/join/park,       (sleep, timed wait/join/park)
         no timeout)            timeout elapses -> RUNNABLE

Java Thread State vs. OS Scheduling

It's important not to conflate Thread.State with what the operating system's scheduler is doing. A JVM thread typically maps to a native OS thread, and the OS scheduler independently decides, moment to moment, whether a RUNNABLE Java thread is actually executing on a CPU core or simply sitting ready in a run queue. Java's RUNNABLE state covers both situations — the JVM does not expose a separate 'currently executing on a core' state, because that level of detail is an OS/hardware scheduling concern, not a language-level guarantee.

This is JVM-implementation territory: HotSpot's typical mapping of Java threads to platform threads, and the exact scheduling policy the OS applies, are implementation details — not something the Java Language Specification mandates. Virtual threads (Java 21+) add a further layer, where many virtual threads are multiplexed onto a smaller pool of platform (carrier) threads by the JVM's own scheduler, covered in their own topic.

Interrupts and State

Calling interrupt() on a thread that is WAITING or TIMED_WAITING (blocked in sleep(), wait(), or join()) causes it to wake immediately with an InterruptedException, transitioning it back toward RUNNABLE (from which it typically then exits or handles the interruption). Calling interrupt() on a thread that is simply RUNNABLE and running ordinary code does not stop it — it only sets the thread's interrupt status flag, which the running code must check itself (e.g. via Thread.interrupted() or isInterrupted()).

interrupt() on a TIMED_WAITING thread throws InterruptedException inside it right away, rather than waiting for the full 10 seconds.

Thread t = new Thread(() -> {
    try {
        Thread.sleep(10_000);
    } catch (InterruptedException e) {
        System.out.println("woken early by interrupt");
    }
});
t.start();
Thread.sleep(200);
t.interrupt(); // wakes the sleeping thread immediately

Practical Usage: Diagnosing Stuck Threads

Thread.getState() is primarily a diagnostic tool — checking whether a background task is progressing, sleeping, or deadlocked (a thread stuck BLOCKED forever is a strong deadlock symptom). Tools like jstack and thread dumps report exactly these six states per thread, which is why understanding them matters even if application code rarely calls getState() directly.

A typical thread-dump excerpt: the BLOCKED state and the exact monitor and holder are visible, which is exactly how deadlocks are diagnosed in production.

"contender" #12 prio=5 BLOCKED on java.lang.Object@1a2b3c4 owned by "holder" #11
   java.base/java.lang.Thread.State: BLOCKED (on object monitor)
        at MyClass.method(MyClass.java:20)
        - waiting to lock <0x000...> (a java.lang.Object)

Quick Reference

NEW: constructed, not started. • RUNNABLE: started, executing or eligible to execute (JVM does not distinguish 'on CPU' from 'ready'). • BLOCKED: waiting to enter a synchronized monitor. • WAITING: suspended with no timeout (wait(), join(), park()). • TIMED_WAITING: suspended with a timeout (sleep(), timed wait/join/park). • TERMINATED: run() finished; permanent, cannot restart.

Key Takeaways

Thread.State has exactly six values: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED.

RUNNABLE covers both 'actually executing' and 'ready and waiting for a CPU' — Java does not expose a separate state for that OS-level distinction.

BLOCKED is specifically for contention on a synchronized intrinsic monitor; java.util.concurrent locks produce WAITING/TIMED_WAITING instead.

A thread cannot be restarted once it is TERMINATED, and calling start() twice on the same Thread object throws IllegalThreadStateException.

The exact mapping of JVM threads to OS scheduling is a JVM/OS implementation detail, not a language guarantee.

Thread t = new Thread(task); t.start(); t.start(); // called again "just in case"

Thread t = new Thread(task); t.start(); // call exactly once

Calling start() more than once on the same Thread object throws IllegalThreadStateException; if the same work needs to run again, construct a new Thread.

if (thread.getState() == Thread.State.RUNNABLE) { // assume it's actively using a CPU right now }

// RUNNABLE only means "eligible to run"; use profiling/thread-dump tools // to know whether it is actually executing on a core at a given instant.

RUNNABLE conflates 'currently executing' and 'ready, waiting for a CPU slice' — Java does not expose the finer OS-level distinction.

while (!thread.isAlive()) { } // spin-wait for it to finish thread.join(); // block until it terminates, or join(timeoutMillis) with a bound isAlive() is false both before a thread starts and after it terminates, so it can't reliably signal completion; join() is the correct API to wait for termination.

List the six values of Thread.State and briefly describe each.

NEW (constructed, not started), RUNNABLE (started, executing or eligible to execute), BLOCKED (waiting to enter a synchronized monitor), WAITING (suspended indefinitely by wait/join/park with no timeout), TIMED_WAITING (suspended with a timeout via sleep/timed wait/timed join/parkNanos), TERMINATED (run() has completed).

Why doesn't RUNNABLE mean a thread is definitely executing on a CPU core right now?

The JVM's RUNNABLE state only says the thread is eligible to run — it is the operating system scheduler that decides, moment to moment, which eligible thread actually gets CPU time. Java does not expose that finer-grained scheduling detail as a separate state.

What specifically causes a thread to enter the BLOCKED state, as opposed to WAITING?

BLOCKED happens only when a thread is trying to enter a synchronized method or block guarded by an intrinsic monitor that another thread currently holds. WAITING happens when a thread voluntarily suspends itself (via wait(), join(), or park()) with no timeout, waiting for another thread to notify, join-target-finish, or unpark it.

Can a TERMINATED thread be restarted by calling start() on it again?

No. Once a Thread object reaches TERMINATED, calling start() on it again throws IllegalThreadStateException. A new Thread object must be created to run the same or different work again.

What happens to a thread's state if interrupt() is called while it is sleeping (TIMED_WAITING via Thread.sleep)?

It wakes immediately, sleep() throws InterruptedException inside it, and its state transitions back toward RUNNABLE as it resumes executing (typically inside a catch block handling the interruption).

Why is isAlive() insufficient to detect that a thread has finished?

isAlive() returns false both before the thread has started (NEW) and after it has terminated (TERMINATED) — it cannot distinguish those two cases, so relying on it to detect 'has finished' after having previously started the thread requires also knowing you already called start().

What Thread.State prints, and why?

TIMED_WAITING. The thread acquired the Thread.class monitor and is now inside Thread.sleep(1000) — sleeping with a timeout, not blocked on a lock (no other thread is contending for that monitor here), so it reports TIMED_WAITING rather than BLOCKED.

Write a small program with two threads where one thread can be observed in the BLOCKED state because it is waiting on a synchronized block held by the other.

public class BlockedDemo { static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread holder = new Thread(() -> { synchronized (lock) { try { Thread.sleep(2000); } catch (InterruptedException ignored) {} } }); Thread contender = new Thread(() -> { synchronized (lock) { System.out.println("contender got the lock"); } }); holder.start(); Thread.sleep(200); // let holder acquire the lock first contender.start(); Thread.sleep(200); // let contender attempt to acquire it System.out.println("contender state: " + contender.getState()); // BLOCKED holder.join(); contender.join(); } } contender's state prints BLOCKED while holder still owns the lock inside its sleep(); once holder's synchronized block exits, contender acquires the lock, runs, and terminates.

What are the two printed states, in order?

NEW, then TERMINATED. Before start() the thread has never run and reports NEW; join() blocks main until t's empty run() completes, so by the time the second println executes t has already finished and reports TERMINATED.

Thread t = new Thread(() -> {
    synchronized (Thread.class) {
        try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
    }
});
t.start();
Thread.sleep(100);
System.out.println(t.getState());
Thread t = new Thread(() -> {});
System.out.println(t.getState());
t.start();
t.join();
System.out.println(t.getState());