Definition
- NEW – the
Threadobject has been created butstart()has not been called yet. - RUNNABLE – the thread is eligible to run; it may be executing on a CPU core or waiting in the OS scheduler queue for its turn.
- BLOCKED – the thread is waiting to acquire a monitor lock that another thread currently holds (trying to enter a
synchronizedblock). - WAITING – the thread has voluntarily paused and will not resume until another thread sends a specific signal:
notify(),notifyAll(),unpark(), or the joined thread terminates. - TIMED_WAITING – the thread has voluntarily paused with a timeout; it resumes either when signaled or when the timeout expires.
- TERMINATED – the
run()method has completed, either normally or by throwing an uncaught exception. The thread cannot be restarted.
Think of a bank teller's workday. NEW is the teller who has been hired but has not shown up for the first day yet. RUNNABLE is the teller at the counter, either serving a customer or waiting for the next one in the queue. BLOCKED is the teller standing outside the vault door because another teller is inside. WAITING is the teller sitting in the break room until the manager calls. TIMED_WAITING is the teller on a 30-minute lunch break who comes back automatically. TERMINATED is the teller who has retired and cannot be un-retired.
Understanding the Concepts
What are the six thread states?
The Thread.State enum defines exactly six states. Java does not distinguish between "running" and "ready to run" at the language level: both are RUNNABLE.
package com.bank.core;
public class ThreadStateDemo {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
try { Thread.sleep(1000); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}, "BankWorker");
System.out.println("After creation: " + t.getState()); // NEW
t.start();
System.out.println("After start: " + t.getState()); // RUNNABLE
Thread.sleep(100);
System.out.println("During sleep: " + t.getState()); // TIMED_WAITING
t.join();
System.out.println("After join: " + t.getState()); // TERMINATED
}
}
After creation: NEW
After start: RUNNABLE
During sleep: TIMED_WAITING
After join: TERMINATED
How do state transitions work?
The transitions follow strict rules. A thread can only move forward through the lifecycle. It never goes from TERMINATED back to NEW.
start()moves the thread from NEW to RUNNABLE.- Entering a
synchronizedblock when the lock is held moves the thread to BLOCKED. wait(),join(), orLockSupport.park()moves the thread to WAITING.sleep(ms),wait(ms), orjoin(ms)moves the thread to TIMED_WAITING.- Completing
run()or throwing an uncaught exception moves the thread to TERMINATED.
How do you observe each state in code?
package com.bank.core;
public class AllStatesDemo {
private static final Object vaultLock = new Object();
public static void main(String[] args) throws InterruptedException {
// BLOCKED state: t2 tries to enter synchronized held by t1
Thread t1 = new Thread(() -> {
synchronized (vaultLock) {
try { Thread.sleep(3000); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}, "VaultHolder");
Thread t2 = new Thread(() -> {
synchronized (vaultLock) {
System.out.println("t2 acquired vault lock");
}
}, "VaultWaiter");
t1.start();
Thread.sleep(100);
t2.start();
Thread.sleep(100);
System.out.println("t2 state: " + t2.getState()); // BLOCKED
// WAITING state: t3 calls wait()
Thread t3 = new Thread(() -> {
synchronized (vaultLock) {
try { vaultLock.wait(); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}, "Waiter");
t1.join();
t3.start();
Thread.sleep(100);
System.out.println("t3 state: " + t3.getState()); // WAITING
// Wake t3 so the program can exit
synchronized (vaultLock) { vaultLock.notifyAll(); }
t2.join();
t3.join();
}
}
t2 state: BLOCKED
t2 acquired vault lock
t3 state: WAITING
Why can you not restart a terminated thread?
Once a thread reaches the TERMINATED state, calling start() throws IllegalThreadStateException. The internal native resources have been released. To run the same task again, create a new Thread object.
package com.bank.core;
public class RestartDemo {
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> System.out.println("Processing trade");
Thread t = new Thread(task);
t.start();
t.join();
try {
t.start(); // IllegalThreadStateException!
} catch (IllegalThreadStateException e) {
System.out.println("Cannot restart: " + e.getMessage());
}
// Create a new thread with the same Runnable
Thread t2 = new Thread(task);
t2.start();
t2.join();
}
}
Processing trade
Cannot restart: null
Processing trade
How is RUNNABLE different from actually running?
Java merges "running on a CPU core" and "ready to run but waiting for CPU time" into the single RUNNABLE state. The OS scheduler decides which RUNNABLE threads actually get CPU time. On a machine with 4 cores, at most 4 threads can truly execute simultaneously. The rest sit in the scheduler's ready queue but are still reported as RUNNABLE.
How do you use thread states for monitoring?
In production banking systems, monitoring thread states reveals performance bottlenecks.
- Many BLOCKED threads on the same lock indicate lock contention. The fix is to reduce the synchronized scope, use
ReadWriteLock, or switch to lock-free structures. - Many TIMED_WAITING threads are normal during low traffic: pool workers are idle.
- Zero RUNNABLE threads with a full task queue indicates a deadlock. Take a thread dump immediately with
jstack.
package com.bank.core;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class ThreadMonitor {
public static void printStateSummary() {
ThreadMXBean tmx = ManagementFactory.getThreadMXBean();
ThreadInfo[] infos = tmx.dumpAllThreads(false, false);
long blocked = 0, waiting = 0, runnable = 0;
for (ThreadInfo info : infos) {
switch (info.getThreadState()) {
case BLOCKED -> blocked++;
case WAITING, TIMED_WAITING -> waiting++;
case RUNNABLE -> runnable++;
default -> { }
}
}
System.out.println("RUNNABLE=" + runnable
+ " BLOCKED=" + blocked + " WAITING=" + waiting);
}
}
What should a senior engineer know?
- Thread dumps:
jstackorkill -3on Linux produces a thread dump showing every thread's state and stack trace. This is the primary tool for diagnosing deadlocks and contention. - RUNNABLE does not mean CPU-busy: a thread doing a blocking
Socket.read()appears as RUNNABLE because the JVM does not distinguish between CPU work and native IO waits. - Virtual thread states (Java 21): virtual threads also use the same
Thread.Stateenum, but a virtual thread in WAITING does not consume an OS thread. It is unmounted from the carrier thread.
Visual Overview
start()
NEW ──────────────────▶ RUNNABLE
│ ▲
┌─────────────┘ │
│ │ lock acquired
▼ │
trying to enter ┌─────┴─────┐
synchronized ──────▶│ BLOCKED │
└───────────┘
RUNNABLE
│ ▲
│ │ notify() / unpark() / join completes
▼ │
┌────────────────┐
│ WAITING │ wait(), join(), park()
└────────────────┘
RUNNABLE
│ ▲
│ │ timeout expires / notify()
▼ │
┌────────────────┐
│ TIMED_WAITING │ sleep(ms), wait(ms), join(ms)
└────────────────┘
RUNNABLE ──── run() ends ────▶ TERMINATED
(permanent)
Key Takeaway
A Java thread passes through six states defined by the Thread.State enum. It starts as NEW, becomes RUNNABLE when start() is called, and ends as TERMINATED when run() completes. Between RUNNABLE and TERMINATED, the thread may temporarily enter BLOCKED (involuntary wait for a monitor lock), WAITING (voluntary indefinite wait for a signal), or TIMED_WAITING (voluntary wait with a timeout).
The most common confusion is between BLOCKED and WAITING. A BLOCKED thread is stuck at a synchronized entry point because another thread holds the lock. A WAITING thread chose to pause by calling wait(), join(), or park(). BLOCKED resolves automatically when the lock is released. WAITING requires an explicit signal from another thread.
Monitoring thread states in production is essential. A spike in BLOCKED threads signals lock contention. Many TIMED_WAITING threads during low traffic is normal. No RUNNABLE threads with a full queue means deadlock.
Remember one sentence: Java threads move through NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED, and once terminated, a thread cannot be restarted.
---