ArchitectMind
  1. Home
  2. Tutorials
  3. Book 2 · JVM, Concurrency & Performance
  4. Volume IV
  5. Thread States
Book 2 · Volume IV — Java Memory Model & Concurrency · Topic 1.3

Thread States

NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED

8 min readStage 1 · Foundations7 interview questions
Prerequisites
1.1 Process vs Thread
Covers
NEW · RUNNABLE · BLOCKED · WAITING · TIMED_WAITING · TERMINATED
Related
Chapter 3 (lifecycle) · Chapter 4 (monitors) · B2V3 5.2 jstack
On this page
  1. 1. Concept / Definition
  2. 2. Why It Exists
  3. 3. Core Concepts
  4. 4. How It Works Internally
  5. 5. APIs / Features / Syntax
  6. 6. Practical Examples
  7. 7. Common Problems & Mistakes
  8. 8. Performance & Production Considerations
  9. 9. Architecture & Design Considerations
  10. 10. Interview Questions & Answers

1Concept / Definition

Every Java thread is at any moment in exactly one of six states defined by java.lang.Thread.State: NEW (created, not started), RUNNABLE (executing or ready to execute in the JVM), BLOCKED (waiting to enter a synchronized monitor), WAITING (waiting indefinitely for another thread's action), TIMED_WAITING (waiting with a timeout) and TERMINATED (finished). The states are JVM-level abstractions used by Thread.getState(), thread dumps and monitoring tools; they do not map one-to-one to operating-system thread states.

Thread.State transitions
Figure 1 — Thread.State transitions

2Why It Exists

Knowing a thread's state is the first step in diagnosing hangs, slowdowns and deadlocks: a pool full of BLOCKED threads indicates lock contention, WAITING threads in a queue indicate idle workers or waits for results, RUNNABLE threads in socket reads indicate slow downstreams. The state model also documents the precise semantics of each blocking method.

3Core Concepts

Which call puts a thread into which state
Figure 2 — Which call puts a thread into which state

NEW

A Thread object has been created but start() has not been called. No OS thread exists yet. Calling start() a second time on any thread (in any state after NEW) throws IllegalThreadStateException.

RUNNABLE

  • The thread is executing Java code, or is ready and waiting for the OS to give it a CPU.
  • It is also RUNNABLE while executing native code — including blocking socket reads, file I/O and many system calls — because the JVM does not know whether native code is waiting.
  • Therefore: RUNNABLE ≠ consuming CPU. Combine thread dumps with per-thread CPU (top -H, cpu= in dumps) to tell busy from blocked in I/O.
Java's RUNNABLE versus OS states
Figure 3 — Java's RUNNABLE versus OS states

BLOCKED

The thread is waiting to acquire an intrinsic monitor — entering a synchronized block or method whose monitor another thread holds — or to re-acquire the monitor after Object.wait() returns. BLOCKED applies only to monitors; threads waiting for java.util.concurrent locks are WAITING (they are parked).

Entry set and wait set of a monitor
Figure 4 — Entry set and wait set of a monitor

WAITING

  • Object.wait() without timeout (after releasing the monitor), until notify/notifyAll or interruption.
  • Thread.join() without timeout, until the target thread terminates.
  • LockSupport.park() — used by ReentrantLock, Condition.await(), BlockingQueue.take(), CountDownLatch.await(), Future.get(), until unpark or interruption.
  • Idle pool threads waiting in take() are WAITING — normal, not a problem.

TIMED_WAITING

  • Thread.sleep(millis) / sleep(Duration).
  • Object.wait(timeout), Thread.join(timeout) / join(Duration).
  • LockSupport.parkNanos / parkUntil — used by tryLock(timeout), poll(timeout), await(timeout), Future.get(timeout).
  • Scheduled executor threads between tasks and pool threads with keep-alive timeouts.

TERMINATED

run() has completed, normally or by throwing an exception (after the uncaught-exception handler ran). The OS thread is gone; the Thread object remains as long as it is referenced. join() on a terminated thread returns immediately.

4How It Works Internally

HotSpot tracks a lower-level thread state (in Java, in VM, in native, blocked) for safepoints, and separately maintains the java.lang.Thread status field that getState() reports. Monitor contention sets BLOCKED around ObjectMonitor::enter; parking sets WAITING/TIMED_WAITING around Parker::park. Virtual threads have the same states; a blocked virtual thread is unmounted from its carrier and does not occupy an OS thread.

5APIs / Features / Syntax

JAVAObserving thread states
Object lock = new Object();
Thread holder = Thread.ofPlatform().start(() -> { synchronized (lock) { sleepQuietly(5_000); } });
Thread.sleep(100);
Thread contender = Thread.ofPlatform().start(() -> { synchronized (lock) { } });
Thread waiter = Thread.ofPlatform().start(() -> LockSupport.park());
Thread.sleep(100);
// TIMED_WAITING  (sleeping while holding the lock)
System.out.println(holder.getState());
System.out.println(contender.getState());  // BLOCKED        (wants the monitor)
System.out.println(waiter.getState());      // WAITING        (parked)

// Counting states across the JVM
Map<Thread.State, Long> byState = Thread.getAllStackTraces().keySet().stream()
        .collect(Collectors.groupingBy(Thread::getState, Collectors.counting()));
SHELLState statistics from a thread dump
# States in a thread dump (Volume III 5.2)
$ jcmd <pid> Thread.print | grep -o "State: [A-Z_]*" | sort | uniq -c
    212 State: RUNNABLE
     48 State: TIMED_WAITING
    180 State: WAITING
     37 State: BLOCKED           ← contention on some monitor: find "waiting to lock <0x…>"

6Practical Examples

PatternStatesMeaning
request threads piling up on one monitormany BLOCKED, one RUNNABLE ownerlock contention (Chapter 4)
pool idleworkers WAITING in LinkedBlockingQueue.takeno work — normal
requests slow, CPU lowRUNNABLE in socketRead0slow downstream / missing timeouts
all workers TIMED_WAITING in HikariPool.getConnectionconnection-pool starvationpool too small or connection leak
thread never finishesWAITING in join/ get without timeoutadd timeouts; investigate target

7Common Problems & Mistakes

  • Reading RUNNABLE as 'working' — it includes blocking native I/O.
  • Expecting BLOCKED for ReentrantLock waits — they show as WAITING (parked) with 'parking to wait for'.
  • Treating many WAITING threads as a problem — idle pools look like that.
  • Using getState() for synchronisation logic — states are for monitoring only and change immediately.

8Performance & Production Considerations

  • Export thread counts by state (jvm_threads_states_threads{state=…}) and alert on BLOCKED spikes.
  • Always use timeouts on join, get, poll and lock acquisition in production paths to avoid indefinite WAITING.
  • Name threads so state statistics can be grouped by pool.

Taking state snapshots is cheap but not free. A full thread dump brings the JVM to a safepoint and walks every stack, which pauses all threads for milliseconds with a few hundred threads and longer with thousands, so take dumps on demand or every few seconds during an incident, not continuously. Thread.getAllStackTraces and ThreadMXBean.dumpAllThreads have the same cost. For trends, rely on per-state gauges, which only read the status field, and on JFR events such as jdk.JavaMonitorEnter and jdk.ThreadPark, which record waits above a threshold together with the lock involved and the stack, at low overhead.

9Architecture & Design Considerations

Thread states turn into design rules once you look at what each state means for a shared resource. Every thread in BLOCKED or WAITING holds memory and often a pool slot while doing nothing, so a design that lets threads wait without a bound converts any slow dependency into exhaustion of the caller. Bound every wait with a timeout, size pools against downstream capacity rather than against expected load, and isolate dependencies in separate pools or bulkheads so that one slow service cannot leave every request thread waiting.

Contention shows up as BLOCKED or parked threads on one lock, and it is a design smell rather than a tuning problem: shrink critical sections, partition state, or replace a shared lock with concurrent structures or message passing. With virtual threads, WAITING becomes cheap because a parked virtual thread releases its carrier, which favours simple blocking code; the limits then move to connection pools and downstream rate limits, which need explicit semaphores. Finally, make states observable by design: name every pool, export per-state counts per pool and alert on sustained BLOCKED growth.

10Interview Questions & Answers

Q1KnowledgeConcept

What are the six Java thread states?

java.lang.Thread.State defines six states, and a thread is in exactly one at any moment. NEW means the Thread object exists but start() has not been called. RUNNABLE means it is executing in the JVM or ready to run, which includes native code such as blocking socket reads. BLOCKED means it is waiting to enter a synchronized monitor held by another thread. WAITING means it is waiting without a timeout for another thread, through wait, join or park. TIMED_WAITING is the same with a timeout, including sleep. TERMINATED means run() has finished. These are JVM-level states reported by getState() and thread dumps; they do not map one to one onto OS thread states.

Q2UnderstandingConcept

What is the difference between BLOCKED and WAITING?

BLOCKED is narrow: the thread is trying to enter a synchronized block or method, or to re-enter the monitor after Object.wait returns, and another thread holds that monitor. WAITING is broader: the thread has voluntarily suspended itself until another thread acts, through Object.wait without a timeout, Thread.join, or LockSupport.park. Because ReentrantLock, Condition, BlockingQueue, CountDownLatch and Future.get all park, a thread waiting for any java.util.concurrent lock shows as WAITING, not BLOCKED. In a thread dump, BLOCKED appears with waiting to lock and a monitor address, while a parked thread shows parking to wait for and the synchronizer object. Both states mean the thread is not running, but the cause differs.

Q3UnderstandingTricky

Which state does Thread.sleep put a thread in?

Thread.sleep puts the thread in TIMED_WAITING until the time elapses or it is interrupted, in which case it throws InterruptedException and clears the interrupt flag. The trap is that sleep does not release anything the thread holds. If it sleeps inside a synchronized block, other threads trying to enter that monitor sit in BLOCKED for the whole sleep, and the same applies to a held ReentrantLock, whose waiters show as WAITING. Object.wait(timeout) is different: it also results in TIMED_WAITING, but it releases the monitor while waiting and must re-acquire it afterwards. So never sleep while holding a lock, and use a condition or wait with a timeout instead.

Q4ApplicationTricky

Why is a thread blocked in a socket read shown as RUNNABLE?

Because the JVM does not know what native code is doing. A blocking socket read in a platform thread ends in a native call such as read or poll, and HotSpot only records that the thread is in native. Thread.State is defined in terms of Java-level waiting, through monitors, wait, join and park, so a thread inside native code is reported as RUNNABLE whether it is computing or sitting in the kernel. That makes RUNNABLE a poor proxy for busy. To tell the difference, check per-thread CPU time with top -H or the cpu field in thread dumps, and set read timeouts on every client so these threads cannot wait forever.

Q5ApplicationInternals

What state is a thread in right after wait() returns due to notify?

It goes to BLOCKED first, not RUNNABLE. Object.wait releases the monitor and puts the thread in the monitor's wait set, in WAITING or TIMED_WAITING. notify or notifyAll moves it from the wait set towards the entry set, but the notifying thread still holds the monitor, typically until it leaves its synchronized block. The woken thread must re-acquire the monitor before wait can return, so until then it competes with other threads and reports BLOCKED. Once it wins the monitor it becomes RUNNABLE and continues after the wait call. This is why waits belong in a loop that re-checks the condition: another thread may have changed it in between.

Q6Senior EngineerScenario

How can thread states help diagnose production issues?

I take several thread dumps a few seconds apart and compare them, because one dump is only a snapshot. Counting threads by state and grouping them by pool name and top frames turns a dump into a diagnosis. Many BLOCKED threads waiting on the same monitor, with one RUNNABLE owner, mean lock contention. Workers in TIMED_WAITING inside a connection pool's getConnection mean pool starvation or a leak. RUNNABLE threads stuck in socketRead or in native frames with low CPU mean a slow downstream or missing timeout. WAITING threads in join or Future.get without a timeout point to an indefinite wait. Per-state metrics then show when the pattern started.

Q7Technical ArchitectDesign

Why should production code use timed waits?

Because an indefinite wait hands control of your thread to someone else. If a dependency hangs or a result never arrives, a thread in join, Future.get, BlockingQueue.take or a lock acquisition stays WAITING forever, and under load every request thread ends up there, exhausting the pool and making the whole service unresponsive rather than just one feature. A timeout bounds that damage: the thread returns, the caller can fail fast, retry, fall back or report a clear error, and the pool keeps serving other work. Set timeouts from latency budgets, shorter than the caller's own timeout, and combine them with circuit breakers so repeated timeouts stop hitting the failing dependency.

Newsletter

Get new tutorials every week

New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.

Almost done! Check your inbox and click the confirmation link to complete your subscription.