If you do not understand thread states, thread dumps will look like random noise.
You will see RUNNABLE, WAITING, or BLOCKED and still have no idea whether the system is healthy or stuck.
This post gives the lifecycle model you need before deeper concurrency topics.
Problem Statement
A production incident says:
- request latency is rising
- CPU is low
- thread dump shows many threads in
WAITING
Is that healthy backpressure, an idle pool, a deadlock precursor, or a dependency stall?
Without thread-state literacy, you cannot answer.
Naive Mental Model
Many developers think a thread is either:
- running
- or not running
Java thread behavior is more nuanced. Threads pass through multiple states depending on creation, scheduling, locking, waiting, sleeping, and completion.
The Java Thread Lifecycle
The main thread states are:
NEWRUNNABLEBLOCKEDWAITINGTIMED_WAITINGTERMINATED
NEW
Thread object created, but start() not yet called.
RUNNABLE
Thread is eligible to run. It may actually be executing, or ready to execute when scheduled.
BLOCKED
Thread is waiting to acquire a monitor lock.
This usually points to synchronized contention.
WAITING
Thread waits indefinitely for another thread’s action. Examples:
Object.wait()Thread.join()LockSupport.park()
TIMED_WAITING
Thread waits for a bounded duration. Examples:
Thread.sleep(...)wait(timeout)join(timeout)
TERMINATED
Thread completed execution.
Why RUNNABLE Confuses People
In Java, RUNNABLE does not mean “currently on CPU right now.”
It means the thread is in a runnable state from the JVM’s perspective.
That means:
- it could be actively executing
- it could be waiting for CPU scheduling
- it could even be in native work that still maps to a runnable status
This is why thread dumps need interpretation, not just scanning.
Runnable Example
This program creates threads that visit several states.
import java.util.concurrent.TimeUnit;
public class ThreadStateDemo {
private static final Object MONITOR = new Object();
public static void main(String[] args) throws Exception {
Thread sleepingThread = new Thread(() -> sleep(3000), "sleeping-thread");
Thread lockHolder = new Thread(() -> {
synchronized (MONITOR) {
sleep(3000);
}
}, "lock-holder");
Thread blockedThread = new Thread(() -> {
synchronized (MONITOR) {
System.out.println("Acquired monitor");
}
}, "blocked-thread");
System.out.println("Before start: " + sleepingThread.getState()); // NEW
sleepingThread.start();
lockHolder.start();
sleep(200);
blockedThread.start();
sleep(300);
System.out.println("Sleeping thread: " + sleepingThread.getState());
System.out.println("Lock holder: " + lockHolder.getState());
System.out.println("Blocked thread: " + blockedThread.getState());
sleepingThread.join();
lockHolder.join();
blockedThread.join();
System.out.println("After completion: " + sleepingThread.getState()); // TERMINATED
}
static void sleep(long millis) {
try {
TimeUnit.MILLISECONDS.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
Typical observations:
- before
start(), thread isNEW - a sleeping thread is
TIMED_WAITING - a thread waiting for a monitor is
BLOCKED - after completion, thread is
TERMINATED
Production Interpretation Guide
Many BLOCKED threads
Usually means monitor contention. Look for:
- broad
synchronizedsections - shared singleton bottlenecks
- slow work inside monitor-held paths
Many WAITING threads
This may be normal or bad.
Normal:
- idle executor workers waiting for tasks
- threads parked by coordination utilities
Bad:
- stuck joins
- lost-notification bugs
- threads waiting forever for an event that will never happen
Many TIMED_WAITING threads
Often benign:
- scheduled retries
- polling loops
- backoff logic
But it may also mean wasted threads sleeping instead of using proper coordination.
Production-Style Example
Imagine a report service:
- request threads submit report jobs
- worker threads process them
- idle workers wait for new tasks
In a healthy idle period, workers may be WAITING.
That is not a problem.
Now imagine billing refresh holds a global lock while making a slow remote call.
Other threads trying to update shared state may become BLOCKED.
That is a real concurrency smell.
The state alone is not enough. You must combine state with what the thread is waiting on.
Failure Modes and Edge Cases
Common misreadings:
- assuming
RUNNABLEmeans “busy CPU” - assuming
WAITINGmeans deadlock - assuming
TIMED_WAITINGis harmless - ignoring
BLOCKEDchains in hot request paths
A good concurrent engineer reads states in context:
- what code path is involved?
- what lock or condition is involved?
- how many threads show the same pattern?
Testing and Debugging Notes
Useful habits:
- print thread names in learning examples
- capture thread dumps during artificial contention tests
- compare dumps under idle load and peak load
- distinguish pool idleness from request-path blockage
Later in the series, these skills become essential for diagnosing deadlocks, lock contention, and bad executor designs.
Decision Guide
If a design frequently produces BLOCKED threads in request paths, reduce shared lock scope.
If it produces many parked or waiting threads, ask whether the waiting model is explicit and healthy.
If thread states are hard to explain, the concurrency design is probably too opaque.
Key Takeaways
- Java threads move through specific lifecycle states
RUNNABLEis broader than “currently executing”BLOCKED,WAITING, andTIMED_WAITINGmust be interpreted in context- thread-state literacy is essential for debugging production concurrency issues