Think of a thread like a worker in a factory: interviewers ask this because they want to know if you understand the worker's whole day, not just the moments when it is busy.
Question: What is the thread lifecycle in Java?
Answer: In Java, a thread moves through states such as NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. A thread is NEW before start(); after start() it becomes RUNNABLE, which means it can run, not that it is definitely on the CPU right now. It can then pause for a lock, another thread, or a timeout, and finally ends in TERMINATED when run() finishes.
Interview-Ready Answer: I’d explain it this way: a Java thread starts in NEW, moves to RUNNABLE after start(), and then may temporarily go to BLOCKED, WAITING, or TIMED_WAITING depending on whether it is fighting for a monitor lock, waiting indefinitely, or waiting with a timeout. Java does not expose a separate RUNNING state; RUNNABLE covers both ready-to-run and actually running. Once run() completes, the thread is TERMINATED, and you can only call start() once on a thread object.
Java gives each thread a Thread.State value so you can reason about what it is doing. The important mental model is that a thread is not always either “working” or “not working”; it can also be waiting for a lock, waiting for another thread, or sleeping for a timeout. A monitor is the lock attached to an object that synchronized uses.
new Thread(...), the thread is NEW. It exists as an object, but the JVM has not asked the OS to schedule it yet.start() tells the JVM to create a real thread and eventually invoke run(). The state becomes RUNNABLE.RUNNABLE is a broad bucket. In HotSpot JVMs, it covers both a thread that is actually executing and one that is ready but waiting its turn. Java does not show a separate RUNNING state.synchronized block but another thread already owns that monitor, it becomes BLOCKED. It is waiting specifically for a monitor lock.wait(), join() with no timeout, or LockSupport.park(), it enters WAITING. This means it is waiting until some other action wakes it up.sleep(), wait(timeout), or join(timeout), it enters TIMED_WAITING. The thread will wake up after the timeout even if nobody notifies it.run() returns, the thread becomes TERMINATED. A terminated thread cannot be restarted.| Method | Needs monitor? | Releases lock? | Typical state |
|---|---|---|---|
sleep() | No | No | TIMED_WAITING |
wait() | Yes | Yes | WAITING or TIMED_WAITING |
join() | No | N/A | WAITING or TIMED_WAITING |
Why this matters: sleep() pauses the current thread but keeps any locks it already holds, while wait() releases the lock and lets other threads enter the synchronized section. That one difference is the source of many bugs.
O(1), but the real cost comes from scheduling and context switches.Thread.sleep(100) is not a precise timer; the actual wake-up time can be later because the scheduler decides when the thread runs again.start() can be called only once. Calling it twice throws IllegalThreadStateException.sleep(), wait(), or join() by throwing InterruptedException.Memory hook: Think “New, Ready, Stuck, Waiting, Ticking, Terminated” — a thread is created, becomes eligible, may get stuck on a lock, may wait forever or for time, and finally ends.
Imagine an e-commerce checkout service with a pool of worker threads handling payment and inventory updates. One worker grabs a database row lock inside a synchronized block or a database transaction, then does slow work inside that critical section. Other workers line up and become BLOCKED, while some request-processing threads wait on a queue or join on a background task and become WAITING or TIMED_WAITING.
What goes wrong when someone misunderstands the lifecycle? A developer uses sleep() instead of wait() while holding a lock, thinking it will “let others continue.” It does not release the monitor, so every other thread stays blocked behind that lock. In production, you see rising checkout latency, thread dumps full of BLOCKED workers, low CPU usage, and logs like “timeout waiting for payment confirmation” even though the machine is not overloaded.
The fix is usually to shorten the synchronized section, release the monitor before long waits, and use the right coordination tool for the job.
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ThreadLifecycleDemo {
public static void main(String[] args) throws Exception {
final Object lock = new Object();
CountDownLatch holderEntered = new CountDownLatch(1);
Thread newThread = new Thread(() -> { }, "newbie");
Thread runner = new Thread(() -> {
// Busy work keeps the thread eligible to run.
// Java reports this as RUNNABLE; there is no separate public RUNNING state.
long end = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(300);
while (System.nanoTime() < end) {
// Intentional empty loop for demo purposes.
}
}, "runner");
Thread sleeper = new Thread(() -> sleepQuietly(500), "sleeper");
Thread joiner = new Thread(() -> {
try {
// join() without a timeout puts the current thread into WAITING
// until the target thread finishes.
sleeper.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "joiner");
Thread holder = new Thread(() -> {
synchronized (lock) {
holderEntered.countDown();
// Holding the monitor here makes other threads BLOCKED if they
// try to enter the same synchronized section.
sleepQuietly(800);
}
}, "holder");
Thread blocker = new Thread(() -> {
synchronized (lock) {
// This line runs only after holder releases the monitor.
}
}, "blocker");
System.out.println("NEW state before start: " + newThread.getState());
runner.start();
sleeper.start();
joiner.start();
holder.start();
// Give the JVM a moment to schedule the threads so the sampled states are visible.
Thread.sleep(100);
System.out.println("runner state: " + runner.getState()); // usually RUNNABLE
System.out.println("sleeper state: " + sleeper.getState()); // TIMED_WAITING because of sleep()
System.out.println("joiner state: " + joiner.getState()); // WAITING because of join()
holderEntered.await();
blocker.start();
Thread.sleep(100);
System.out.println("blocker state: " + blocker.getState()); // BLOCKED on the monitor
runner.join();
sleeper.join();
joiner.join();
holder.join();
blocker.join();
System.out.println("runner final state: " + runner.getState()); // TERMINATED
System.out.println("joiner final state: " + joiner.getState()); // TERMINATED
// Edge case: a thread object can be started only once.
Thread oneShot = new Thread(() -> { }, "oneShot");
oneShot.start();
try {
oneShot.start();
} catch (IllegalThreadStateException ex) {
System.out.println("Calling start() twice fails: " + ex.getClass().getSimpleName());
}
oneShot.join();
System.out.println("All threads finished.");
}
private static void sleepQuietly(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
Follow-up & Tricky Questions:
Follow-up questions
NEW and RUNNABLE?NEW after construction but before start(). After start(), it becomes RUNNABLE, meaning the JVM has asked the OS to schedule it.RUNNING state?RUNNABLE.BLOCKED and WAITING?BLOCKED means the thread is trying to enter a synchronized block but another thread owns the monitor. WAITING means the thread voluntarily waited, such as on join() or wait(), and needs another action to continue.start() twice?IllegalThreadStateException. A thread object is one-shot; once it has started, it cannot be restarted after completion.interrupt() interact with lifecycle states?sleep(), wait(), or join() by throwing InterruptedException. It does not forcefully kill the thread.Tricky / gotcha questions
sleep() release a synchronized lock?sleep() inside a synchronized section often causes contention.WAITING always blocked on a lock?BLOCKED.run() directly instead of start()?Common Mistakes:
RUNNABLE means “currently running.” Correction: it means the thread is eligible to run; Java does not expose a separate running state.sleep() to wait for another thread. Correction: use wait(), join(), futures, or other coordination tools depending on the problem.wait() requires ownership of the monitor. Correction: you must call it inside a synchronized block on the same object, or you get IllegalMonitorStateException.start() twice or calling run() by mistake. Correction: call start() exactly once to create a new thread.Memory Hook: Picture a worker badge: New gets the badge, Runnable stands in line, Blocked is stuck at a locked door, Waiting sits by the phone, Timed Waiting sets a timer, and Terminated goes home.
Cheat Sheet:
NEW = thread object created, not started.start() moves it to RUNNABLE.BLOCKED = waiting for a monitor lock.WAITING = waiting indefinitely, often on join() or wait().TIMED_WAITING = waiting with a timeout, often on sleep() or timed join().TERMINATED = run() finished, cannot restart.Practice Tasks:
start().wait()/notifyAll() instead of join().start() twice, then catch and print the exception.