Java Tutorial
🔍

Thread Lifecycle

Thread Lifecycle

Every Thread object moves through a well-defined set of states over its lifetime, reported by Thread.getState() as one of six values in the Thread.State enum: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED. Knowing what each one actually means — and what it does not — is what turns a confusing hang or a stuck thread dump into something diagnosable.

What Is the Thread Lifecycle?

The thread lifecycle is the fixed sequence of states the JVM tracks for every Thread object, from the moment it is constructed to the moment its run() method finishes. Thread.getState() reports exactly one of the six values at any given time, and understanding what each one actually means — not just its name — is what separates a developer who can read a thread dump from one who can only guess at it.

Why Thread Safety Matters

Most real concurrency bugs eventually show up as threads stuck in a state they should not be in — dozens of threads BLOCKED on the same lock, or a pool of workers stuck WAITING on a condition that will never fire. None of that is diagnosable without knowing precisely what each state means and what causes a thread to enter it.

BLOCKED and WAITING look similar from the outside — a thread doing nothing — but they mean completely different things. BLOCKED means lock contention with a specific competing thread; WAITING means a deliberate pause with no guaranteed competitor at all. Confusing the two during an incident is a common way to chase the wrong root cause.

Two of the six states are always safe to observe with total certainty: NEW, before a thread has ever been started, and TERMINATED, once join() has returned.

1// File: ThreadLifecycleBasics.java 2 3public class ThreadLifecycleBasics { 4 public static void main(String[] args) throws InterruptedException { 5 Thread worker = new Thread(() -> { 6 try { 7 Thread.sleep(50); 8 } catch (InterruptedException e) { 9 Thread.currentThread().interrupt(); 10 } 11 }); 12 13 System.out.println("Before start(): " + worker.getState()); 14 15 worker.start(); 16 worker.join(); 17 18 System.out.println("After join(): " + worker.getState()); 19 } 20}
Output:
Before start(): NEW
After join(): TERMINATED

A freshly constructed Thread is always NEW — nothing about its eventual work has happened yet. join() blocks until the thread has actually finished, so by the time it returns, TERMINATED is guaranteed.

How It Works

A short sentence before the diagram: this is the full path a thread can take between construction and termination, including every branch out of RUNNABLE and back into it.

NEW
 |
 | start()
 v
RUNNABLE ----------------------------+
 |  |  |                             |
 |  |  +-- enters synchronized,     BLOCKED
 |  |      lock held elsewhere       |
 |  |                                | lock released
 |  +-- Thread.sleep() /            TIMED_WAITING
 |      timed wait() / timed join()  |
 |                                   | timeout ends, or notified
 +-- wait() / join(), no timeout -- WAITING
                                     |
                                     | notified, or joined thread ends
                                     v
                              (back to RUNNABLE, then eventually)
                                     |
                                     v
                                TERMINATED

RUNNABLE is the hub every other transient state branches out from and eventually returns to — the four states between NEW and TERMINATED are each observable by having the worker thread signal exactly when it reaches the relevant point, using a CountDownLatch, plus a short buffer on the observing side.

Code Examples

RUNNABLE means eligible to run — either actually executing on a CPU core right now, or ready and waiting for the operating system to grant it one. Java's Thread.State does not distinguish between those two cases.

1// File: RunnableStateExample.java 2import java.util.concurrent.CountDownLatch; 3import java.util.concurrent.atomic.AtomicLong; 4 5public class RunnableStateExample { 6 public static void main(String[] args) throws InterruptedException { 7 CountDownLatch started = new CountDownLatch(1); 8 AtomicLong sum = new AtomicLong(); 9 10 Thread worker = new Thread(() -> { 11 started.countDown(); 12 long total = 0; 13 for (long i = 0; i < 200_000_000L; i++) { 14 total += i; 15 } 16 sum.set(total); 17 }); 18 19 worker.start(); 20 started.await(); 21 22 System.out.println("State while computing: " + worker.getState()); 23 24 worker.join(); 25 System.out.println("Loop finished, sum computed: " + (sum.get() > 0)); 26 } 27}
Output:
State while computing: RUNNABLE
Loop finished, sum computed: true

A thread inside Thread.sleep(), or waiting on a lock or condition with a timeout, is TIMED_WAITING.

1// File: TimedWaitingExample.java 2import java.util.concurrent.CountDownLatch; 3 4public class TimedWaitingExample { 5 public static void main(String[] args) throws InterruptedException { 6 CountDownLatch aboutToSleep = new CountDownLatch(1); 7 8 Thread worker = new Thread(() -> { 9 aboutToSleep.countDown(); 10 try { 11 Thread.sleep(300); 12 } catch (InterruptedException e) { 13 Thread.currentThread().interrupt(); 14 } 15 }); 16 17 worker.start(); 18 aboutToSleep.await(); 19 Thread.sleep(50); 20 21 System.out.println("State during sleep: " + worker.getState()); 22 23 worker.join(); 24 } 25}
Output:
State during sleep: TIMED_WAITING

A thread that has called Object.wait() with no timeout, or Thread.join() with no timeout, is WAITING — parked indefinitely until something else wakes it.

1// File: WaitingStateExample.java 2import java.util.concurrent.CountDownLatch; 3 4public class WaitingStateExample { 5 private static final Object LOCK = new Object(); 6 7 public static void main(String[] args) throws InterruptedException { 8 CountDownLatch aboutToWait = new CountDownLatch(1); 9 10 Thread worker = new Thread(() -> { 11 synchronized (LOCK) { 12 aboutToWait.countDown(); 13 try { 14 LOCK.wait(); 15 } catch (InterruptedException e) { 16 Thread.currentThread().interrupt(); 17 } 18 } 19 }); 20 21 worker.start(); 22 aboutToWait.await(); 23 Thread.sleep(50); 24 25 System.out.println("State while waiting: " + worker.getState()); 26 27 synchronized (LOCK) { 28 LOCK.notify(); 29 } 30 worker.join(); 31 } 32}
Output:
State while waiting: WAITING

A thread stuck trying to enter a synchronized block or method that another thread currently holds is BLOCKED — distinct from WAITING, since BLOCKED is specifically about contention for a lock, not a deliberate wait.

1// File: BlockedStateExample.java 2import java.util.concurrent.CountDownLatch; 3 4public class BlockedStateExample { 5 private static final Object LOCK = new Object(); 6 7 public static void main(String[] args) throws InterruptedException { 8 CountDownLatch aboutToBlock = new CountDownLatch(1); 9 10 Thread worker = new Thread(() -> { 11 aboutToBlock.countDown(); 12 synchronized (LOCK) { 13 // never reached until main releases the lock below 14 } 15 }); 16 17 synchronized (LOCK) { 18 worker.start(); 19 aboutToBlock.await(); 20 Thread.sleep(50); 21 22 System.out.println("State while blocked: " + worker.getState()); 23 } 24 25 worker.join(); 26 } 27}
Output:
State while blocked: BLOCKED

Real-World Example

An airport check-in kiosk runs passenger processing on a background thread, and a monitor reports exactly what that worker is doing at each point in its lifecycle — the same technique applies to diagnosing a stuck or slow background task, verifying a thread has actually finished before reading data it produced, and building a lightweight health check that reports state instead of a plain alive/dead boolean.

1// File: CheckInWorker.java 2import java.util.concurrent.CountDownLatch; 3 4public class CheckInWorker extends Thread { 5 private final CountDownLatch processingStarted = new CountDownLatch(1); 6 7 @Override 8 public void run() { 9 processingStarted.countDown(); 10 try { 11 Thread.sleep(200); 12 } catch (InterruptedException e) { 13 Thread.currentThread().interrupt(); 14 } 15 } 16 17 public void awaitProcessingStarted() throws InterruptedException { 18 processingStarted.await(); 19 } 20}
1// File: CheckInKioskMonitor.java 2 3public class CheckInKioskMonitor { 4 public static void main(String[] args) throws InterruptedException { 5 CheckInWorker worker = new CheckInWorker(); 6 System.out.println("Before starting: " + worker.getState()); 7 8 worker.start(); 9 worker.awaitProcessingStarted(); 10 Thread.sleep(50); 11 System.out.println("While processing: " + worker.getState()); 12 13 worker.join(); 14 System.out.println("After completion: " + worker.getState()); 15 } 16}
Output:
Before starting: NEW
While processing: TIMED_WAITING
After completion: TERMINATED

A mistake that appears often in fresher pull requests is polling a background worker's isAlive() in a tight loop to guess what it is doing, when getState() already reports exactly which of the six lifecycle states the thread is actually in — RUNNABLE, TIMED_WAITING, BLOCKED, and so on — without needing to infer anything from indirect signals. BLOCKED state is exactly what piles up when multiple threads contend for the same lock, tying this topic directly to this section's Synchronization and Deadlock articles.

Best Practices

Reach for getState() when diagnosing a stuck or slow thread rather than guessing from timing or isAlive() alone — it directly names which of the six states the thread is actually in.

Treat RUNNABLE as "eligible to run," not "definitely executing right now" — a RUNNABLE thread on a busy system may simply be waiting for the operating system to grant it a CPU core.

Use join() to wait for a specific thread to reach TERMINATED rather than polling getState() in a loop, which wastes CPU and adds unnecessary complexity for something join() already does correctly.

Distinguish BLOCKED from WAITING when reading a thread dump — BLOCKED points at lock contention with a specific competing thread, while WAITING points at a deliberate pause with no direct competitor to investigate.

Common Mistakes

Treating RUNNABLE as proof a thread is actively executing on a CPU core right now is a common misunderstanding — the JVM's Thread.State does not have a separate state for "waiting for a CPU core" versus "actually running," so a busy system can leave a RUNNABLE thread sitting unscheduled for a meaningful stretch of time.

Calling start() on a Thread object more than once throws IllegalThreadStateException — a thread cannot be restarted once it has been started, even after it has terminated.

1// File: DoubleStartMistake.java 2 3public class DoubleStartMistake { 4 public static void main(String[] args) throws InterruptedException { 5 Thread worker = new Thread(() -> {}); 6 worker.start(); 7 worker.join(); 8 9 try { 10 worker.start(); 11 } catch (IllegalThreadStateException e) { 12 System.out.println("Caught: " + e.getClass().getSimpleName()); 13 } 14 } 15}
Output:
Caught: IllegalThreadStateException

A new Thread object, constructed fresh with the same Runnable, is required for another run — a single Thread instance's lifecycle only ever moves forward, never back to NEW.

Interview Questions

Q1. What are the six states in Java's Thread.State enum?

NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — every Thread object moves through some subset of these over its lifetime, always starting at NEW and ending at TERMINATED. A fresher-level answer stops at naming them; a stronger answer can immediately say what causes each transition.

Q2. What is the difference between BLOCKED and WAITING?

BLOCKED specifically means a thread is stuck trying to enter a synchronized block or method another thread currently holds. WAITING means a thread has deliberately paused itself — via Object.wait() with no timeout, Thread.join() with no timeout, or similar — with no competing thread necessarily involved at all. The nuance interviewers are listening for is "lock contention" versus "deliberate pause," not just two different-sounding names.

Q3. Does RUNNABLE mean a thread is actually executing on a CPU core right now?

Not necessarily. RUNNABLE means eligible to run — the thread may be actively executing, or it may be ready and simply waiting for the operating system's scheduler to grant it a core. Java's Thread.State does not distinguish between the two.

Q4. What state is a thread in immediately after being constructed but before start() is called?

NEW, always and without exception — this is demonstrated with full certainty in this article's ThreadLifecycleBasics example, since no work has happened yet for a thread that has never been started.

Q5. Can a thread be started more than once?

No. Calling start() on a Thread that has already been started — whether it is still running or has already terminated — throws IllegalThreadStateException, exactly as demonstrated in this article's Common Mistakes section.

Q6. What state does a thread enter while inside Thread.sleep()?

TIMED_WAITING, since sleep() pauses for a bounded duration rather than indefinitely — the same state applies to a timed wait() or a timed join(). Interviewers at product companies often follow up by asking what distinguishes this from WAITING, which is exactly the boundedness of the pause.

Q7. How does calling join() relate to the TERMINATED state?

join() blocks the calling thread until the target thread has actually finished running, which is precisely the definition of TERMINATED — so by the time join() returns, getState() on that thread is guaranteed to report TERMINATED.

FAQs

Is there a RUNNING state distinct from RUNNABLE?

No. Java's Thread.State has no separate RUNNING value — a thread that is actively executing and one that is merely eligible to run and waiting for a CPU core both report as RUNNABLE.

What state is a thread in while waiting to enter a synchronized block another thread holds?

BLOCKED, exactly as demonstrated in this article's BlockedStateExample — this is the state most associated with lock contention and deadlock diagnosis.

Does Thread.getState() require any special permission or setup to call?

No, it is an ordinary public method on Thread, callable from any code that has a reference to the Thread object, with no special setup required.

Can a terminated thread be observed with getState() after it has finished?

Yes, indefinitely — once a thread reaches TERMINATED, getState() continues to report TERMINATED for as long as the Thread object itself is reachable, even long after the thread has actually finished running.

What's the difference between WAITING and TIMED_WAITING?

WAITING is an indefinite pause with no built-in expiration — something else must explicitly wake the thread. TIMED_WAITING is the same idea with a maximum duration, after which the thread wakes on its own even if nothing else notifies or interrupts it.

Does calling interrupt() on a thread change its Thread.State?

Not directly by itself. If the thread is currently blocked in a timed or untimed wait, interrupt() causes that wait to end with an InterruptedException, which then leads to whatever state transition the thread's own code causes next — interrupt() does not set a state on its own the way start() or wait() do.

Is Thread.State the same set of states an operating system reports for its own native threads?

No, not necessarily. Thread.State is a JVM-level abstraction describing Java's view of a thread's lifecycle — the underlying operating system may model its own native threads with a different set of states entirely, which the JVM maps onto its own six values.

Summary

Every Java thread moves through the same six-state lifecycle — NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, and TERMINATED — and Thread.getState() reports exactly which one a thread is in at any moment, turning what might otherwise be a guessing game around a stuck or slow thread into something directly diagnosable.

The habit worth carrying forward from this article's kiosk monitor example is reaching for getState() specifically, rather than inferring a thread's situation from isAlive() or timing alone, and remembering that RUNNABLE only ever means "eligible to run," never a guarantee of actual, immediate CPU execution.

What to Read Next