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.”

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:
| State | Meaning |
|---|---|
NEW | Thread object created, start() not yet called |
RUNNABLE | Running on CPU or ready to run (waiting for CPU) |
BLOCKED | Waiting to acquire an intrinsic lock (monitor) |
WAITING | Waiting indefinitely until another thread signals it |
TIMED_WAITING | Waiting for a specified duration |
TERMINATED | run() has finished (normally or with an exception) |
Note: The
RUNNABLEstate 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 ofstart()does NOT create a new thread — it simply calls the method on the current thread. Always usestart(). 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 asynchronizedblockThread.join()— waiting for another thread to finishLockSupport.park()— low-level parking used byjava.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 awhileloop that re-checks the condition, not anif. Spurious wake-ups (rare but permitted by the JVM spec) can cause subtle bugs if you useif. 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 locksObject.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, useObject.wait(millis)inside asynchronizedblock 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
| Trigger | From → To |
|---|---|
thread.start() | NEW → RUNNABLE |
| OS scheduler picks thread | RUNNABLE (ready) → RUNNABLE (running) |
synchronized lock unavailable | RUNNABLE → BLOCKED |
| Lock becomes available | BLOCKED → RUNNABLE |
Object.wait() / LockSupport.park() | RUNNABLE → WAITING |
notify() / notifyAll() / unpark() | WAITING → RUNNABLE (or BLOCKED if re-acquiring lock) |
Thread.sleep(n) / wait(n) | RUNNABLE → TIMED_WAITING |
| Timeout expires or early notify | TIMED_WAITING → RUNNABLE |
run() returns | RUNNABLE → TERMINATED |
Related Topics
- Creating a Thread — the two ways to create and start a thread in Java
- Thread.sleep() — pause execution and understand TIMED_WAITING in practice
- Synchronization — how monitors cause BLOCKED states and how to use them correctly
- Deadlock — what happens when threads are stuck in BLOCKED forever
- Inter-Thread Communication — using wait/notify to move threads between WAITING and RUNNABLE
- Virtual Threads (Project Loom) — how Java 21 changes the cost of BLOCKED and WAITING states