When a production service hangs, the first diagnostic anyone takes is a thread dump — and a
thread dump is just a list of threads with their states. If you can read BLOCKED, WAITING
and TIMED_WAITING fluently, you can often spot the problem in minutes. That is why the thread
lifecycle is asked in almost every Java interview: it tests whether you understand what threads
are doing, not just how to create them.
Java defines the lifecycle precisely: the Thread.State enum has exactly six values, and
getState() tells you which one a thread is in right now.
The six states at a glance
| State | Meaning | Entered by | Leaves when |
|---|---|---|---|
NEW |
Created, not started | new Thread(...) |
start() is called |
RUNNABLE |
Running or ready to run | start(), or waking from any pause |
Scheduler decision or a pause |
BLOCKED |
Waiting for a monitor lock | Contending for synchronized |
Lock becomes available |
WAITING |
Paused indefinitely | wait(), join(), park() |
notify(), target dies, unpark() |
TIMED_WAITING |
Paused with a deadline | sleep(ms), wait(ms), join(ms) |
Timeout expires or woken early |
TERMINATED |
Finished | run() returns or throws |
Never — it is final |
Two facts about this table earn instant credibility in interviews. First, there is no
RUNNING state — Java folds "executing right now" and "ready, waiting for CPU" into
RUNNABLE, because the OS scheduler owns that distinction. Second, NEW and TERMINATED are
visited exactly once; everything between them can cycle many times per second.
NEW and RUNNABLE: birth and readiness
A thread object begins life as a plain Java object. Until you call start(), no OS thread
exists — getState() returns NEW and the task has not run a single instruction. start()
does three things: asks the OS for a real thread, moves the state to RUNNABLE, and arranges
for run() to be invoked on that new thread.
public class NewToTerminated {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Task running on " + Thread.currentThread().getName());
});
System.out.println("After construction: " + worker.getState()); // NEW
worker.start();
worker.join(); // wait for it to finish
System.out.println("After join: " + worker.getState()); // TERMINATED
}
}
Run this and you see the two endpoints of every thread's life: NEW before start(),
TERMINATED after run() returns. Try adding worker.start() a second time — you get
IllegalThreadStateException, proof that the lifecycle is strictly one-way.
Common mistake: A common mistake beginners make is treating a
Threadobject as reusable. Once terminated, a thread can never run again; you must construct a new one. This is exactly why thread pools exist — they keep threads alive and feed them new tasks instead of dying after one.
BLOCKED: queued at a monitor
A thread is BLOCKED when it tries to enter a synchronized block whose monitor lock another
thread currently holds. It is not sleeping and not waiting for a signal — it is queued at the
lock and will proceed the instant the lock frees. Here is a program that manufactures the
state deliberately:
public class BlockedDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
Thread holder = new Thread(() -> {
synchronized (LOCK) {
pause(1000); // hold the lock for a full second
}
});
Thread contender = new Thread(() -> {
synchronized (LOCK) {
System.out.println("contender finally got the lock");
}
});
holder.start();
Thread.sleep(100); // let holder grab the lock first
contender.start();
Thread.sleep(100); // give contender time to hit the lock
System.out.println("contender state: " + contender.getState()); // BLOCKED
holder.join();
contender.join();
}
private static void pause(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
The output shows contender state: BLOCKED — it wants the monitor that holder owns. In a
thread dump, many threads BLOCKED on the same lock is the signature of lock contention, and
two threads BLOCKED on each other's locks is a
deadlock. Note how the InterruptedException is handled
by restoring the interrupt flag — swallowing it silently is a bug pattern covered in
exception handling in Java.
WAITING vs TIMED_WAITING: paused on purpose
Both states mean the thread chose to pause; the difference is whether there is a deadline.
WAITING threads sleep until another thread acts — notify() after a wait(), the target
thread finishing after a join(), or LockSupport.unpark(). TIMED_WAITING threads carry a
wake-up alarm: Thread.sleep(500), wait(2000), join(1000).
public class WaitingStates {
public static void main(String[] args) throws InterruptedException {
Thread sleeper = new Thread(() -> pause(2000)); // TIMED_WAITING
sleeper.start();
Thread joiner = new Thread(() -> {
try {
sleeper.join(); // WAITING (no timeout)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
joiner.start();
Thread.sleep(200); // let both threads settle into their pauses
System.out.println("sleeper: " + sleeper.getState()); // TIMED_WAITING
System.out.println("joiner: " + joiner.getState()); // WAITING
sleeper.join();
joiner.join();
}
private static void pause(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
One thread, two pauses, two different states: sleeper has a deadline so it is
TIMED_WAITING; joiner waits open-endedly for sleeper to die, so it is WAITING. Both
return to RUNNABLE when their condition resolves — they do not jump straight back onto the
CPU, they rejoin the queue.
Interview note: The sharpest follow-up here is "which state releases the lock?" —
wait()releases the monitor it was called on (that is its entire purpose), whileThread.sleep()andjoin()release nothing. A sleeping thread inside asynchronizedblock still holds the lock and blocks everyone else.
TERMINATED: the end, normal or not
A thread reaches TERMINATED when run() returns normally or when an uncaught exception
kills it. The second path surprises people: the exception terminates only that thread, prints
a stack trace, and the rest of the program continues. If that thread was doing something
critical — consuming a queue, writing a heartbeat — the application limps along half-alive.
Register a Thread.UncaughtExceptionHandler (or use an executor, which lets you inspect the
failure through the returned Future) so thread deaths never go unnoticed.
Transitions interviewers draw on the whiteboard
The classic diagram question: trace one worker thread through a busy second of its life.
new Thread(task)→ NEWstart()→ RUNNABLE (scheduler may run it immediately or not)- Hits a
synchronizedblock owned by another thread → BLOCKED - Gets the lock, calls
wait(500)on a condition → TIMED_WAITING (lock released) - Woken by
notify()→ re-acquires the lock, possibly BLOCKED again briefly → RUNNABLE run()returns → TERMINATED
Step 5 is the detail that impresses: a notified thread cannot resume inside the
synchronized block until it re-acquires the monitor, so WAITING frequently transitions
through BLOCKED on the way back. If you can explain that hop, you understand the lifecycle
better than most candidates at the
2-years-experience level.
Pro tip: You do not need a debugger to explore this.
jstack <pid>(orjcmd <pid> Thread.print) dumps every thread's state in a running JVM. Run it against any Spring Boot app and practice classifying the states you see — that is exactly the skill the lifecycle question is testing. How locks and monitors create these states in the first place is the subject of synchronization in Java.
Frequently Asked Questions
What is the difference between BLOCKED and WAITING in Java?
Is there a RUNNING state in Java?
Can a thread go back to NEW or be restarted after TERMINATED?
Which methods put a thread into TIMED_WAITING?
What state is a thread in while performing blocking I/O?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

