skip to content

Why does Java's RUNNABLE state cover both a thread actively running and a thread blocked on I/O, and what are the consequences for diagnosing performance with thread dumps?

level: seniorimportance: should knowfreq 35%

answer

  1. RUNNABLE = on-CPU OR ready OR blocked-on-I/O
  2. JVM doesn't model OS scheduling or kernel I/O waits
  3. socketRead0/epollWait under RUNNABLE = idling on network, not CPU
  4. Read stack frames + per-thread CPU time, not the state label
  5. Virtual threads target exactly this I/O-wait inefficiency

basics

~20 s

Java's RUNNABLE lumps together threads on a CPU, threads queued to run, and threads waiting on I/O, because the JVM can't reliably tell these apart from the OS. So 'RUNNABLE' in a dump doesn't prove a thread is actually using CPU.

solid answer

~50 s

Java's Thread.State has no separate states for 'running', 'ready', or 'blocked on I/O' — all three are RUNNABLE. The reason is portability and observability: whether a runnable thread is on a core or in the OS run queue is the operating system's business, and a thread blocked inside a native socket read looks, to the JVM, like it's just executing native code. The JVM doesn't model OS-level I/O waits, so it reports RUNNABLE. The diagnostic consequence is big: in a thread dump, a thread shown RUNNABLE deep in a `socketRead0` or `epollWait` frame is NOT burning CPU — it's waiting on the network. So you can't equate 'RUNNABLE count' with 'CPU demand'. To find real CPU hogs you correlate thread dumps with per-thread CPU time (e.g. top -H, async-profiler), and you read the actual stack frames rather than trusting the state label.

go deeper

for a junior

Knows RUNNABLE means 'running or able to run' and that it isn't the same as BLOCKED/WAITING.

for a middle

Recognizes that I/O-blocked threads appear RUNNABLE and that the state alone doesn't prove CPU usage.

for a senior

Explains why the JVM merges running/ready/I/O-wait, and diagnoses dumps by reading native frames plus per-thread CPU time rather than trusting the label.

for a principal

Ties the RUNNABLE-overload to OS scheduling boundaries and the motivation for virtual threads, and builds observability practices (JFR/profilers) that separate on-CPU from off-CPU work.

## The puzzle Java's `Thread.State` enum has **RUNNABLE** but no `RUNNING`, no `READY`, and no `IO_WAIT`. Yet at the OS level those are genuinely different conditions. Why the coarse mapping, and what does it cost you? ## What 'RUNNABLE' actually means in Java The Javadoc defines RUNNABLE as: a thread executing in the JVM that **may** be waiting for an OS resource such as a processor. Read that carefully — it deliberately covers: 1. A thread **actually on a CPU core** right now. 2. A thread **ready and queued** by the OS scheduler, waiting only for a core. 3. A thread **blocked on I/O** (a socket read, file read, etc.). ### Why merge running and ready? The distinction between 'currently on a core' and 'queued, waiting for a core' is owned entirely by the **OS scheduler**. The JVM has no portable, reliable way to observe it, and it changes nanosecond to nanosecond. Modeling it would make the state racy and OS-specific, so Java collapses both into RUNNABLE. ### Why does I/O-wait count as RUNNABLE? When a Java thread does, say, `socket.getInputStream().read()`, it eventually calls a **native** method (`socketRead0`) that traps into the kernel and the thread sleeps until data arrives. But Java's state model only knows about *Java-level* blocking constructs — monitors (BLOCKED) and `wait`/`join`/`park` (WAITING/TIMED_WAITING). It has **no notion of kernel I/O blocking**, so from the JVM's perspective the thread is 'executing native code' = RUNNABLE. (This is exactly the wait that virtual threads were designed to make cheap by unmounting the carrier.) ## The diagnostic trap Because RUNNABLE is overloaded, **a RUNNABLE thread is not necessarily consuming CPU.** Two pitfalls: 1. **Mistaking I/O waits for CPU work.** A thread dump full of RUNNABLE threads parked in `socketRead0`/`epollWait` is *not* a CPU problem — it's threads idling on the network. Throwing more CPU at it won't help; the bottleneck is downstream latency or connection-pool sizing. 2. **Counting RUNNABLE ≠ measuring demand.** The number of RUNNABLE threads overstates real CPU pressure if many are I/O-bound. ## How to diagnose correctly - **Read the stack frames, not the label.** A RUNNABLE thread sitting in `socketRead0`, `epollWait`, `read`, or similar native frames is waiting on I/O. A RUNNABLE thread in your hot business-logic loop or a tight `compute()` is the real CPU consumer. - **Correlate with per-thread CPU time.** Tools like `top -H -p <pid>` (Linux), `jstack` + native thread IDs, or a sampling profiler (async-profiler, JFR) show which threads actually accumulate CPU time. JFR/async-profiler distinguish on-CPU from off-CPU. - **Take multiple dumps.** A thread genuinely RUNNABLE-and-on-CPU will show *moving* stacks across successive dumps; an I/O-waiter shows the *same* native frame repeatedly. ## The takeaway RUNNABLE is a **coarse** label by design — it trades precision for portability and stability. Treat it as 'not blocked on a Java-level lock/wait', not as 'using CPU'. Real performance triage reads the frames and measures CPU time, never just the state column.

  • How do platform threads blocked on I/O differ from virtual threads doing the same?
    A platform thread blocked in native I/O holds its OS thread (shown RUNNABLE, idle). A virtual thread blocking on I/O unmounts from its carrier platform thread, freeing the carrier to run other virtual threads — so blocking I/O no longer pins an OS thread.
  • Given a thread dump, how do you tell a CPU-bound RUNNABLE thread from an I/O-bound one?
    Inspect the top stack frames: native I/O frames (socketRead0, epollWait) mean I/O wait; application/compute frames mean CPU work. Confirm with per-thread CPU time and by comparing successive dumps for stack movement.

saying these in an interview costs you the question

  • Assuming every RUNNABLE thread is consuming CPU.
  • Believing Java has a separate I/O-wait or RUNNING state.
  • Diagnosing a CPU bottleneck purely from the RUNNABLE thread count.
  • Thinking socket I/O shows as BLOCKED or WAITING in a dump.

context