Skip to content
Java multithreading 6 min read

Thread Life Cycle

Every Java thread follows a well-defined journey: it is born, it competes for CPU time, it may pause for various reasons, and eventually it finishes. Knowing these states helps you write correct concurrent code, diagnose bugs like deadlocks, and understand why a thread is “stuck.”

Thread life cycle state diagram

The Six Thread States

Java defines thread states in the Thread.State enum (introduced in Java 5). A thread is always in exactly one of these states:

StateMeaning
NEWThread object created, start() not yet called
RUNNABLERunning on CPU or ready to run (waiting for CPU)
BLOCKEDWaiting to acquire an intrinsic lock (monitor)
WAITINGWaiting indefinitely until another thread signals it
TIMED_WAITINGWaiting for a specified duration
TERMINATEDrun() has finished (normally or with an exception)

Note: The RUNNABLE state covers both “currently executing on a CPU core” and “eligible to run but waiting for the OS scheduler.” Java does not expose these two sub-states separately.

State Diagram

         NEW
          │  start()

      RUNNABLE  ◄──────────────────────────────────────────┐
     /    │    \                                            │
    │     │     └──────────────────────────────────┐       │
    │  (runs)                                      │       │
    │     │                                        ▼       │
    │     │                                     BLOCKED ───┘
    │     │           wait() / join()           (lock released)
    │     ├─────────────────────────► WAITING ──► (notify / notifyAll)
    │     │
    │     │     sleep() / wait(ms) / join(ms)
    │     ├─────────────────────────► TIMED_WAITING ──► (timeout / notify)
    │     │
    │     ▼
    │  TERMINATED
    │  (run() finishes)

1. NEW

A thread enters the NEW state when you create a Thread object but have not yet called start(). No OS thread exists yet — it is just a Java object on the heap.

Thread t = new Thread(() -> System.out.println("Hello from thread"));
System.out.println(t.getState()); // NEW

Output:

NEW

2. RUNNABLE

Calling start() hands the thread to the JVM thread scheduler. From this point the thread is either actively running on a CPU core, or queued and ready to run. You cannot distinguish the two sub-states from Java code.

Thread t = new Thread(() -> {
    System.out.println("State inside run: " + Thread.currentThread().getState());
});
t.start();
t.join(); // wait for completion

Output:

State inside run: RUNNABLE

Tip: Calling run() directly instead of start() does NOT create a new thread — it simply calls the method on the current thread. Always use start(). See run() vs start() for a detailed comparison.

3. BLOCKED

A thread moves to BLOCKED when it tries to enter a synchronized block or method that is currently held by another thread. It sits here until the lock becomes available, at which point it returns to RUNNABLE.

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(3000); } catch (InterruptedException e) {}
            }
        });

        Thread waiter = new Thread(() -> {
            synchronized (lock) { // will block until holder releases
                System.out.println("waiter acquired the lock");
            }
        });

        holder.start();
        Thread.sleep(100); // give holder time to grab the lock
        waiter.start();
        Thread.sleep(100); // give waiter time to attempt the lock
        System.out.println("waiter state: " + waiter.getState()); // BLOCKED
        holder.join();
        waiter.join();
    }
}

Output:

waiter state: BLOCKED
waiter acquired the lock

4. WAITING

A thread enters WAITING when it calls one of these methods without a timeout:

  • Object.wait() — inside a synchronized block
  • Thread.join() — waiting for another thread to finish
  • LockSupport.park() — low-level parking used by java.util.concurrent

It stays here indefinitely until another thread calls notify(), notifyAll(), or LockSupport.unpark().

Object signal = new Object();

Thread waiter = new Thread(() -> {
    synchronized (signal) {
        try {
            signal.wait(); // enters WAITING
            System.out.println("waiter woke up!");
        } catch (InterruptedException e) {}
    }
});

waiter.start();
Thread.sleep(200);
System.out.println("waiter state: " + waiter.getState()); // WAITING

synchronized (signal) {
    signal.notify();
}
waiter.join();

Output:

waiter state: WAITING
waiter woke up!

Warning: Always call wait() inside a while loop that re-checks the condition, not an if. Spurious wake-ups (rare but permitted by the JVM spec) can cause subtle bugs if you use if. See Inter-Thread Communication for the full pattern.

5. TIMED_WAITING

Similar to WAITING, but with an explicit timeout. The thread returns to RUNNABLE when the timeout expires or when it is notified early.

Methods that cause TIMED_WAITING:

  • Thread.sleep(millis) — pauses without releasing locks
  • Object.wait(millis)
  • Thread.join(millis)
  • LockSupport.parkNanos() / LockSupport.parkUntil()
Thread sleeper = new Thread(() -> {
    try { Thread.sleep(5000); } catch (InterruptedException e) {}
});

sleeper.start();
Thread.sleep(100);
System.out.println("sleeper state: " + sleeper.getState()); // TIMED_WAITING

Output:

sleeper state: TIMED_WAITING

Note: Thread.sleep() does NOT release any locks the thread holds. If you need to pause while releasing a lock, use Object.wait(millis) inside a synchronized block instead. More detail at Thread.sleep().

6. TERMINATED

Once run() returns — either normally or because an uncaught exception escaped — the thread moves to TERMINATED. A terminated thread cannot be restarted; calling start() on it throws IllegalThreadStateException.

Thread t = new Thread(() -> System.out.println("done"));
t.start();
t.join();
System.out.println("state: " + t.getState()); // TERMINATED

Output:

done
state: TERMINATED

Checking State at Runtime

You can inspect a thread’s state programmatically with Thread.getState(). This is useful for debugging and monitoring tools.

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

System.out.println("Before start: " + worker.getState());   // NEW
worker.start();
Thread.sleep(100);
System.out.println("While running: " + worker.getState());  // TIMED_WAITING (sleeping)
worker.join();
System.out.println("After finish: " + worker.getState());   // TERMINATED

Output:

Before start: NEW
While running: TIMED_WAITING
After finish: TERMINATED

Under the Hood

When you call Thread.start(), the JVM calls the native pthread_create (on Linux) or the equivalent OS API. The Java RUNNABLE state maps to two OS-level states: ready (in the run queue) and running (on a CPU). The JVM intentionally merges these because the transition happens below the JVM abstraction layer and changes thousands of times per second.

Intrinsic locks and BLOCKED: Every Java object has an associated monitor (a mutex in the OS or JVM internals). When a thread fails to acquire it, the JVM parks the thread in the monitor’s entry set — this is the BLOCKED state. The OS scheduler does not give it CPU time until the monitor is released and the thread is moved back to the run queue.

WAITING vs BLOCKED — why two states? BLOCKED always involves competing for a lock. WAITING means the thread voluntarily gave up the CPU and is sitting in the monitor’s wait set, waiting for a notify(). The distinction matters for debugging: a spike in BLOCKED threads usually means lock contention; a spike in WAITING threads is expected if they are idle workers.

Thread dumps: Tools like jstack, VisualVM, or Java Flight Recorder let you capture a thread dump — a snapshot of every thread and its current state. Reading these is an essential skill for diagnosing deadlocks and performance bottlenecks.

Virtual threads (Java 21): Project Loom introduced virtual threads (Virtual Threads) which follow the same Thread.State model, but their BLOCKED/WAITING states are handled by the JVM carrier thread rather than the OS scheduler, making them vastly cheaper to create and park.

Quick Reference: State Transitions

TriggerFrom → To
thread.start()NEWRUNNABLE
OS scheduler picks threadRUNNABLE (ready) → RUNNABLE (running)
synchronized lock unavailableRUNNABLEBLOCKED
Lock becomes availableBLOCKEDRUNNABLE
Object.wait() / LockSupport.park()RUNNABLEWAITING
notify() / notifyAll() / unpark()WAITINGRUNNABLE (or BLOCKED if re-acquiring lock)
Thread.sleep(n) / wait(n)RUNNABLETIMED_WAITING
Timeout expires or early notifyTIMED_WAITINGRUNNABLE
run() returnsRUNNABLETERMINATED
Last updated June 13, 2026
Was this helpful?