Thread Lifecycle
The Java thread lifecycle is the complete sequence of states a thread passes through from the moment a Thread object is constructed to the moment its execution ends. Java defines six states in the Thread.State enum — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and the JVM transitions threads between these states in response to specific method calls, lock acquisitions, monitor notifications, timeouts, and exceptions. Each state has a precise meaning, a defined set of entry conditions, and a defined set of exit conditions. Understanding the lifecycle in full is prerequisite knowledge for diagnosing deadlocks, thread leaks, performance bottlenecks in thread dumps, and incorrect synchronization — all of which manifest as threads stuck in specific states. This entry covers every state in the lifecycle with its entry and exit conditions, all legal and illegal state transitions, how thread dumps represent each state, the interaction between lifecycle states and interruption, the effect of uncaught exceptions on lifecycle, and how to observe lifecycle transitions programmatically.
NEW and RUNNABLE — Birth of a Thread
// ── NEW state: Thread constructed, not yet started ───────────────────
Thread t = new Thread(() -> System.out.println("running"), "demo-thread");
System.out.println(t.getState()); // NEW
System.out.println(t.getName()); // demo-thread — properties readable in NEW
System.out.println(t.getPriority()); // 5
// Properties can be modified while in NEW:
t.setName("renamed-thread");
t.setPriority(Thread.MAX_PRIORITY);
t.setDaemon(true);
// WRONG: calling run() directly — no new thread, runs on calling thread
t.run(); // executes synchronously, thread stays NEW after this returns
System.out.println(t.getState()); // still NEW — run() doesn't change state
// ── start() — transitions NEW → RUNNABLE ─────────────────────────────
Thread t2 = new Thread(() -> {
System.out.println("Executing on: " + Thread.currentThread().getName());
System.out.println("State from inside: " + Thread.currentThread().getState()); // RUNNABLE
}, "started-thread");
System.out.println("Before start: " + t2.getState()); // NEW
t2.start();
// After start() returns, thread is RUNNABLE — may or may not be executing yet:
System.out.println("After start: " + t2.getState()); // RUNNABLE or TERMINATED (very fast tasks)
t2.join();
System.out.println("After join: " + t2.getState()); // TERMINATED
// ── IllegalThreadStateException — restarting a thread ────────────────
Thread once = new Thread(() -> {}, "one-shot");
once.start();
once.join();
try {
once.start(); // COMPILE: fine. RUNTIME: IllegalThreadStateException
} catch (IllegalThreadStateException e) {
System.out.println("Cannot restart: " + e.getClass().getSimpleName());
}
// ── Observing RUNNABLE sub-states from outside the JVM ───────────────
// All of these appear as RUNNABLE to getState():
// 1. Actually running on CPU:
Thread cpuBound = new Thread(() -> {
long x = 0;
while (!Thread.currentThread().isInterrupted()) x++; // 100% CPU — RUNNABLE
}, "cpu-spinner");
// 2. Blocked on OS-level I/O (inside kernel — JVM doesn't know):
Thread ioWaiter = new Thread(() -> {
try {
new java.net.Socket("example.com", 80); // kernel I/O — RUNNABLE from JVM view
} catch (Exception e) {}
}, "io-waiter");
// 3. Ready but not yet scheduled:
Thread ready = new Thread(() -> {}, "not-yet-scheduled");
ready.start(); // may be in ready queue — RUNNABLE but no CPU assigned
// Thread dump shows all three as RUNNABLE with no distinction.
// OS-level tools (async-profiler, perf) can differentiate.
// ── Thread.yield() — a hint, not a guarantee ─────────────────────────
Thread yielder = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("Iteration " + i);
Thread.yield(); // hints to scheduler: others may run now
// State does NOT change — still RUNNABLE throughout
}
}, "yielding-thread");
yielder.start();
yielder.join();BLOCKED, WAITING, and TIMED_WAITING — Suspension States
// ── BLOCKED: waiting to acquire a monitor ────────────────────────────
Object sharedLock = new Object();
Thread[] blockedThreads = new Thread[3];
// Holder acquires and keeps the lock:
Thread holder = new Thread(() -> {
synchronized (sharedLock) {
System.out.println("Holder acquired lock");
try { Thread.sleep(3000); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "lock-holder");
// Three threads try to acquire the same lock simultaneously:
for (int i = 0; i < 3; i++) {
int idx = i;
blockedThreads[i] = new Thread(() -> {
synchronized (sharedLock) { // will BLOCK until holder releases
System.out.println(Thread.currentThread().getName() + " acquired lock");
}
}, "blocked-thread-" + i);
}
holder.start();
Thread.sleep(100); // ensure holder has the lock
for (Thread bt : blockedThreads) bt.start();
Thread.sleep(100); // ensure blocked threads have tried to acquire
for (Thread bt : blockedThreads) {
System.out.println(bt.getName() + ": " + bt.getState()); // BLOCKED
}
holder.interrupt();
for (Thread bt : blockedThreads) bt.join();
// ── WAITING: Object.wait(), join(), LockSupport.park() ───────────────
Object monitor = new Object();
Thread waiter = new Thread(() -> {
synchronized (monitor) {
try {
System.out.println("About to wait...");
monitor.wait(); // releases monitor, enters WAITING
System.out.println("Woken up!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "waiting-thread");
waiter.start();
Thread.sleep(100);
System.out.println(waiter.getState()); // WAITING
synchronized (monitor) {
monitor.notify(); // transitions waiter from WAITING to RUNNABLE
}
waiter.join();
// WAITING via Thread.join():
Thread longTask = new Thread(() -> {
try { Thread.sleep(2000); } catch (InterruptedException e) {}
}, "long-task");
Thread joiner = new Thread(() -> {
try {
System.out.println("Joining long-task...");
longTask.join(); // enters WAITING until longTask terminates
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "joiner-thread");
longTask.start();
joiner.start();
Thread.sleep(100);
System.out.println(joiner.getState()); // WAITING
longTask.interrupt();
joiner.join();
// ── TIMED_WAITING: sleep(), wait(t), join(t), parkNanos() ─────────────
Thread sleeper = new Thread(() -> {
try { Thread.sleep(5000); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "sleeper");
Thread timedWaiter = new Thread(() -> {
synchronized (monitor) {
try { monitor.wait(5000); } catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "timed-waiter");
sleeper.start();
timedWaiter.start();
Thread.sleep(100);
System.out.println(sleeper.getState()); // TIMED_WAITING
System.out.println(timedWaiter.getState()); // TIMED_WAITING
sleeper.interrupt();
timedWaiter.interrupt();
sleeper.join();
timedWaiter.join();
// ── BLOCKED vs WAITING in a thread dump ──────────────────────────────
// BLOCKED appears as:
// "blocked-thread-0" #14 prio=5 os_prio=0
// java.lang.Thread.State: BLOCKED (on object monitor)
// - waiting to lock <0x000000076b3a2e90> (java.lang.Object)
// - locked by "lock-holder" #11
// WAITING appears as:
// "waiting-thread" #15 prio=5 os_prio=0
// java.lang.Thread.State: WAITING (on object monitor)
// - waiting on <0x000000076b3a3120> (java.lang.Object)
// - locked by "waiting-thread" #15 ← owns the monitor, then called wait()
// TIMED_WAITING appears as:
// "sleeper" #16 prio=5 os_prio=0
// java.lang.Thread.State: TIMED_WAITING (sleeping)
// at java.lang.Thread.sleep(Native Method)TERMINATED — End of Life, Uncaught Exceptions, and Observing the Full Lifecycle
// ── TERMINATED: normal completion ────────────────────────────────────
Thread finisher = new Thread(() -> {
System.out.println("About to finish");
}, "finisher");
finisher.start();
finisher.join();
System.out.println(finisher.getState()); // TERMINATED
System.out.println(finisher.isAlive()); // false
// TERMINATED is final — cannot restart:
try {
finisher.start();
} catch (IllegalThreadStateException e) {
System.out.println("Cannot restart TERMINATED thread");
}
// ── TERMINATED via uncaught exception ────────────────────────────────
Thread thrower = new Thread(() -> {
System.out.println("About to throw");
throw new RuntimeException("Intentional failure");
}, "thrower");
thrower.setUncaughtExceptionHandler((thread, ex) -> {
System.err.printf("[UncaughtHandler] Thread '%s' terminated with: %s%n",
thread.getName(), ex.getMessage());
// thread is still alive during handler execution:
System.err.println("State during handler: " + thread.getState()); // RUNNABLE (transitioning)
});
thrower.start();
thrower.join();
System.out.println("After join: " + thrower.getState()); // TERMINATED
// ── join() and the TERMINATED happens-before guarantee ───────────────
int[] result = new int[1];
Thread calculator = new Thread(() -> {
int sum = 0;
for (int i = 1; i <= 1_000_000; i++) sum += i;
result[0] = sum; // write before TERMINATED
}, "calculator");
calculator.start();
calculator.join(); // wait for TERMINATED — happens-before established
System.out.println("Sum: " + result[0]); // safe: join() guarantees visibility
// ── Observing the full lifecycle programmatically ─────────────────────
import java.util.concurrent.CountDownLatch;
Object lifecycleLock = new Object();
CountDownLatch waitingLatch = new CountDownLatch(1);
Thread subject = new Thread(() -> {
// Signal that we're inside run() (RUNNABLE):
synchronized (lifecycleLock) {
lifecycleLock.notifyAll();
}
// Enter WAITING:
try {
waitingLatch.await(); // WAITING via LockSupport.park() inside CountDownLatch
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// Return to RUNNABLE, then TERMINATED
}, "lifecycle-subject");
System.out.println("1. State before start: " + subject.getState()); // NEW
subject.start();
synchronized (lifecycleLock) {
lifecycleLock.wait(); // wait until subject is inside run()
}
System.out.println("2. State after start, inside run(): " + subject.getState()); // RUNNABLE
Thread.sleep(50); // give subject time to reach waitingLatch.await()
System.out.println("3. State while waiting on latch: " + subject.getState()); // WAITING
waitingLatch.countDown(); // release subject
subject.join();
System.out.println("4. State after join: " + subject.getState()); // TERMINATED
// ── Error types and TERMINATED ────────────────────────────────────────
Thread soeThrower = new Thread(() -> {
// StackOverflowError propagates out of run() → TERMINATED
soeThrower_helper(); // will recurse until stack exhausted
}, "soe-thread");
// Uncaught handler fires even for Error:
soeThrower.setUncaughtExceptionHandler((t, ex) ->
System.err.println("Thread terminated with Error: " + ex.getClass().getSimpleName())
);
soeThrower.start();
soeThrower.join();
System.out.println("SOE thread: " + soeThrower.getState()); // TERMINATED
static void soeThrower_helper() { soeThrower_helper(); } // infinite recursion
// ── What a complete thread dump looks like for each state ─────────────
// Full jstack output for a thread in each state:
// NEW (rarely appears in dumps — usually starts fast):
// (not shown — threads in NEW are not yet OS-level threads)
// RUNNABLE:
// "cpu-worker" #12 prio=5 os_prio=0 cpu=1423.45ms elapsed=2.10s
// java.lang.Thread.State: RUNNABLE
// at com.example.Worker.compute(Worker.java:42)
// at com.example.Worker.run(Worker.java:28)
// BLOCKED:
// "db-writer-2" #18 prio=5 os_prio=0 cpu=0.12ms elapsed=1.85s
// java.lang.Thread.State: BLOCKED (on object monitor)
// - waiting to lock <0x00000007163f2d10> (com.example.ConnectionPool)
// - locked by "db-writer-1" #17
// WAITING:
// "event-consumer" #20 prio=5 os_prio=0 cpu=0.08ms elapsed=15.30s
// java.lang.Thread.State: WAITING (parking)
// at sun.misc.Unsafe.park(Native Method)
// at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
// at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
// TIMED_WAITING:
// "pool-1-thread-3" #22 prio=5 os_prio=0 cpu=0.05ms elapsed=300.10s
// java.lang.Thread.State: TIMED_WAITING (parking)
// at sun.misc.Unsafe.parkNanos(Native Method)
// at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
// TERMINATED:
// (not shown in thread dumps — terminated threads are not OS threads)