Java Thread Lifecycle
The states a Java thread can be in — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED — and how they change.
Java Thread Lifecycle
A Java thread doesn't have many states — six, all of them values of the Thread.State enum. But those six are the vocabulary of thread dumps, profilers, and every "why is my program stuck" investigation you'll ever do. Knowing what each state means and what transitions are possible is what turns a thread dump from a wall of stack traces into a diagnosis.
The six states
public enum Thread.State {
NEW, // created, never started
RUNNABLE, // started; running or ready to run on a CPU
BLOCKED, // waiting for a monitor lock to enter a synchronized block
WAITING, // parked indefinitely (Object.wait, Thread.join, LockSupport.park)
TIMED_WAITING, // parked with a timeout (sleep, wait(ms), join(ms), park(nanos))
TERMINATED // run() has returned
}The transitions allowed between them form a simple lifecycle:
NEW
| start()
v
RUNNABLE <----------+--------+-------+
| | | | |
| | enters | wakes | timeout
| v sync | from | expires
| BLOCKED | wait/ |
| | | join |
| | acquires | |
| v lock | |
+-> RUNNABLE | |
| | |
| wait/join/park | |
v | |
WAITING -------------+ |
| |
| wait(ms)/join(ms)/sleep |
v |
TIMED_WAITING ----------------+
|
| run() returns
v
TERMINATEDEach state corresponds to something visible in a thread dump. Let's walk them.
NEW
A Thread you've constructed but never called start() on. No OS resources have been allocated; nothing is running. The only transitions out are:
start()→RUNNABLE- The thread is garbage-collected without ever running
You can call start() exactly once. A second call throws IllegalThreadStateException.
RUNNABLE
"The thread is alive and is either running on a CPU right now or is ready to run." Java collapses the OS's "running" and "runnable" into a single state — there's no way to tell from Thread.State alone whether the thread is currently consuming CPU. The OS scheduler decides which RUNNABLE threads actually get a core at any moment.
A RUNNABLE thread is also the state a thread is in when it's blocked on I/O (InputStream.read, Socket.read, FileChannel.read). This surprises people: the thread is "ready to run" only in the sense that nothing in the JVM is gating it. The OS knows the thread is waiting for the disk; the JVM doesn't, so it reports RUNNABLE. If you see a thread dump where a thread is RUNNABLE and its top frame is socketRead0 or similar, the thread is blocked on a syscall — not burning CPU.
BLOCKED
The thread is sitting at the door of a synchronized block waiting for the monitor lock. Some other thread has it; this one queued up. As soon as the holder releases, one of the waiters wins the lock and transitions back to RUNNABLE.
BLOCKED is specific to synchronized — the intrinsic-lock mechanism the JVM bakes in. Code waiting on a ReentrantLock does not show BLOCKED; it shows WAITING (because ReentrantLock is implemented on top of LockSupport.park). That's a small but important distinction when you're reading dumps.
The classic thread-dump signature for BLOCKED:
"worker-3" #19 prio=5 ... waiting for monitor entry
java.lang.Thread.State: BLOCKED (on object monitor)
at com.acme.Cache.put(Cache.java:42)
- waiting to lock <0x000000076ab8e220> (a java.util.HashMap)
at com.acme.Cache.miss(Cache.java:67)Two pieces of information: which monitor you're waiting on (<0x000000076ab8e220>) and what method is sitting at the door. Search the same dump for - locked <0x000000076ab8e220> and you've found the thread that holds it.
WAITING
The thread chose to wait indefinitely. Three things put a thread here:
Object.wait()— releases the monitor and parks until somebody callsnotify/notifyAll.Thread.join()— without a timeout, parks until the target thread terminates.LockSupport.park()— the primitive on whichReentrantLock.lock(),await(),BlockingQueue.take(), and all ofjava.util.concurrentare built.
A WAITING thread is using essentially no resources beyond its stack. It will not transition until someone else does something — a notify, a join target finishing, a LockSupport.unpark. If nothing ever unparks it, it sits there forever. That's how silent deadlocks look in a dump: two threads, both WAITING, both holding what the other wants.
TIMED_WAITING
Same idea as WAITING, but with a deadline. The thread will wake itself when the timeout expires even if nothing else happens. Things that produce TIMED_WAITING:
Thread.sleep(ms)Object.wait(ms)Thread.join(ms)LockSupport.parkNanos(...),LockSupport.parkUntil(...)BlockingQueue.poll(timeout, unit),Future.get(timeout, unit), etc.
If a thread is reliably stuck in TIMED_WAITING for the duration you specified, that's not a bug. If it stays there past the timeout, it's been re-parked — somebody called wait(1000) in a loop or the queue's still empty.
TERMINATED
run() returned (normally or by exception). The thread is done; it cannot be restarted. t.isAlive() returns false. You can still read its name and ID for log/debug purposes, but the thread itself is finished.
Reading state from your own code
Thread.State is publicly queryable, but the value is a snapshot — it can change between the call and your use of it. In production, you almost never branch on it; you use it for logging and diagnostics. The JVM also exposes ThreadMXBean for full thread dumps, which is what most JMX dashboards display.
Thread t = new Thread(() -> doWork(), "worker");
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (or TIMED_WAITING/BLOCKED/etc., racy)
t.join();
System.out.println(t.getState()); // TERMINATEDA worked example: observe every state
The program below creates threads that each get stuck in a different state, then prints what state they're in.
What to take from the run:
- Each of the six states was reachable by code in the same program.
NEWandTERMINATEDare the boundary cases; the middle four (RUNNABLE,BLOCKED,WAITING,TIMED_WAITING) are the ones you'll see in a real thread dump. - The
blocked-threadreportedBLOCKEDbecause the holder owned thesynchronizedmonitor. Had we used aReentrantLockinstead, the same code path would have reportedWAITING(sinceLock.lock()parks viaLockSupport). The state name tells you what flavour of waiting, not just "this thread is stuck." - The
waiting-threadwould have stayed inWAITINGforever ifmainhadn't calledcond.notify(). TheWAITINGstate has no timeout — somebody else has to wake it. This is exactly how a missednotifyproduces a deadlock that no exception ever reports. - The CPU-burning thread reported
RUNNABLEwhether it was actually running on a core or merely sitting in the run queue waiting for one. The JVM doesn't distinguish "running" from "ready"; the OS does. If you need to know which threads are actually consuming CPU, profile with a sampling profiler —getState()won't tell you. - After
tRunning.join()returned, its state wasTERMINATED. You can still query its name, ID, and state object, but the thread is gone —isAlive()isfalseandstart()would throw. Threads are single-use: when one terminates, you create a new one. (This is the main motivation forExecutorService— it reuses the same OS thread for many tasks.)
What's next
The next chapter, Java Thread Methods, walks the method surface of Thread — sleep, join, yield, interrupt, holdsLock, and the static utilities — with the gotchas for each one.
Practice
A thread is in state `BLOCKED`. What is it waiting for?