Blocking and Non-Blocking I/O
Purpose
Keep four distinct properties distinct, because every confused architecture argument about
virtual threads and reactive programming comes from collapsing them:
Blocking API the method returns when the operation is done
Blocked OS thread a kernel-schedulable entity is parked and unavailable
Non-blocking I/O the syscall returns immediately with whatever is ready
Asynchronous model the code is expressed as callbacks or stages, not statements
A virtual thread doing socket.read() uses a blocking API, does not dedicate a
blocked carrier to that request, sits on top of non-blocking I/O plus a shared poller in
the current JDK implementation, and is written in a
synchronous model. All four at once. Any sentence that treats them as the same axis is
wrong somewhere.
Compatibility and evidence
Virtual-thread APIs require Java 21 (standard, no preview flags). Implementation guidance
here targets HotSpot JDK 21–25; the monitor behavior changes at JDK 24. Before applying it,
inspect compiler/toolchain settings, the deployed JDK vendor and version, OS, resolved
client/framework versions, transport and scheduler configuration. These can differ from
the development JDK. Recheck implementation claims for other releases; do not upgrade the
project or add instrumentation dependencies merely to apply this skill.
Workflow
- Ask which property the claim is about. "Is this blocking?" is four questions;
answer the one that determines the decision at hand.
- Classify each I/O call on the path: does it unmount, capture the carrier with
compensation, or pin? The three have different costs and different fixes.
- Check for file system I/O. Identify the concrete synchronous path and whether the
JDK marks it for compensation; a package name alone does not establish this.
- Check for foreign code: JNI, FFM, a driver with a native transport. A native frame
pins, and pinning is not compensated.
- If an event loop is involved, find every blocking call inside it — one is enough to
stall every connection that loop serves.
- Measure before concluding. Carrier count over time,
jdk.VirtualThreadPinned, and a
wall-clock profile answer this; reasoning about the library's name does not.
Missing events do not rule out native blocking. Without runtime evidence, return a
conditional classification and the measurement that would confirm or refute it.
Rules
- A blocking API is not necessarily a blocked carrier. On a virtual thread,
InputStream.read on a supported socket path registers interest with the JDK's poller,
parks the virtual thread, unmounts, and frees the carrier. Kernel poller threads still
wait for readiness; the point is that there is no blocked OS thread per socket request.
- Under the hood, socket channels are put in non-blocking mode by the JDK and a small
number of dedicated poller threads (
epoll/kqueue) unpark virtual threads when a file
descriptor becomes ready. That is an implementation detail, not a specification — do not
build a design on it, but do use it to explain observations.
- Many synchronous filesystem paths retain the carrier in the target HotSpot releases.
Recognized blocking regions can trigger compensation, temporarily expanding the
scheduler up to
jdk.virtualThreadScheduler.maxPoolSize. Do not generalize this to every
provider, native call or memory-mapped access; inspect the path and measure.
- Capture with compensation is not pinning. Compensation adds a carrier so throughput
survives, at the cost of memory and OS threads. Pinning — a native frame or a blocking
class initialiser — gets no compensation, so it removes a carrier outright. Raising
maxPoolSize helps the first and does nothing for the second.
- The number of platform threads in the scheduler may therefore legitimately exceed
availableProcessors(). Growth towards maxPoolSize under operations the scheduler
recognizes for compensation is the system working as designed; native-frame pinning is
specifically not compensated. Sustained saturation says the compensated-blocking
workload reached this implementation ceiling, not that raising it is automatically safe.
synchronized no longer pins on JDK 24 and later (JEP 491), and Object.wait unmounts
too. On these releases, replacing synchronized solely for pinning is unnecessary;
-Djdk.tracePinnedThreads was removed and silently does nothing.
- Non-blocking I/O is not the reactive model. A
SocketChannel in non-blocking mode with
a Selector is non-blocking I/O written imperatively. Reactor and RxJava can compose
blocking, non-blocking or in-memory work; neither requires Netty. Identify the actual
transport and execution thread before classifying a pipeline.
- Both models can use the same kernel facilities on a given transport, but their
batching, buffer ownership, syscall cadence and wakeups can differ. Do not infer equal
performance from a shared
epoll/read foundation; measure CPU, allocation, throughput
and tail latency. The architectural difference is where suspended logical state lives and
how it is diagnosed.
- Blocking inside an event loop is a different severity of bug from blocking on a pooled
thread. An event-loop thread serves many connections, so blocking it can stall every
connection assigned to it. Loop count and assignment are framework/configuration details,
not a universal
2 × cores. Combine BlockHound tests with loop-lag and wall-clock evidence.
- A "non-blocking" client library is only non-blocking to the boundary of its own API. A
reactive database driver that hands work to a bounded internal pool has the same ceiling as
a JDBC pool, expressed differently.
- Virtual threads make blocking calls cheap, not free: each in-flight call still
holds a stack on the heap, a connection, a buffer and any lock it took. The scarce
resource moved; it did not disappear.
- Readiness-based socket polling does not characterize every
java.nio API or provider.
Verify the target implementation before claiming io_uring or completion-based I/O;
use io-uring-and-zero-copy for that investigation.
Minimum result
For each consequential finding, identify the call and execution thread, API/model versus
carrier behavior, supporting evidence and remaining uncertainty. Propose the smallest
change with a resource bound and a before/after check (loop lag, carrier/native memory,
throughput and tail latency as relevant). Do not label an unmeasured optimization a fix.
References
- What unmounts and what does not — the operation-by-operation
table, the capture / compensation / pinning distinction with the evidence that separates
them, handling file-heavy workloads, and verifying a third-party client's behaviour. Read
when classifying a path or when carrier count is growing.
- Event loops, pollers and blocking detection — how
the JDK's socket poller works and its tuning knobs, the event-loop model and the cost of
blocking one, BlockHound in tests, and the diagnostics for each model. Read when working on
or debugging an event-loop-based stack.
1---2name: blocking-and-nonblocking-io3description: Four things routinely conflated into one: a blocking API, a blocked OS thread, non-blocking I/O at the syscall, and an asynchronous programming model. Covers which JDK operations unmount a virtual thread and which capture the carrier, the difference between capture-with-compensation and pinning, the socket poller behind blocking socket calls, file I/O as the case Loom does not fix, and what blocking an event loop costs. Use when someone says virtual threads make I/O non-blocking, when a file-heavy workload on virtual threads grows the carrier pool, when a blocking call sits inside a Netty or Reactor pipeline, when jdk.virtualThreadScheduler.maxPoolSize is raised to fix a symptom, or when an argument turns on whether the model or the syscall is the bottleneck. Not choosing between the two models (reactive-and-virtual-thread-selection), continuation mechanics and pinning diagnosis (virtual-threads-internals), demand signalling (reactive-backpressure), or copy avoidance (io-uring-and-zero-copy).4---56# Blocking and Non-Blocking I/O78## Purpose910Keep four distinct properties distinct, because every confused architecture argument about11virtual threads and reactive programming comes from collapsing them:1213```text14Blocking API the method returns when the operation is done15Blocked OS thread a kernel-schedulable entity is parked and unavailable16Non-blocking I/O the syscall returns immediately with whatever is ready17Asynchronous model the code is expressed as callbacks or stages, not statements18```1920A virtual thread doing `socket.read()` uses a **blocking API**, does **not** dedicate a21blocked carrier to that request, sits on top of **non-blocking I/O** plus a shared poller in22the current JDK implementation, and is written in a23**synchronous** model. All four at once. Any sentence that treats them as the same axis is24wrong somewhere.2526## Compatibility and evidence2728Virtual-thread APIs require Java 21 (standard, no preview flags). Implementation guidance29here targets HotSpot JDK 21–25; the monitor behavior changes at JDK 24. Before applying it,30inspect compiler/toolchain settings, the deployed JDK vendor and version, OS, resolved31client/framework versions, transport and scheduler configuration. These can differ from32the development JDK. Recheck implementation claims for other releases; do not upgrade the33project or add instrumentation dependencies merely to apply this skill.3435## Workflow36371. **Ask which property the claim is about.** "Is this blocking?" is four questions;38 answer the one that determines the decision at hand.392. **Classify each I/O call on the path**: does it unmount, capture the carrier with40 compensation, or pin? The three have different costs and different fixes.413. **Check for file system I/O.** Identify the concrete synchronous path and whether the42 JDK marks it for compensation; a package name alone does not establish this.434. **Check for foreign code**: JNI, FFM, a driver with a native transport. A native frame44 pins, and pinning is not compensated.455. **If an event loop is involved, find every blocking call inside it** — one is enough to46 stall every connection that loop serves.476. **Measure before concluding.** Carrier count over time, `jdk.VirtualThreadPinned`, and a48 wall-clock profile answer this; reasoning about the library's name does not.49 Missing events do not rule out native blocking. Without runtime evidence, return a50 conditional classification and the measurement that would confirm or refute it.5152## Rules5354- **A blocking API is not necessarily a blocked carrier.** On a virtual thread,55 `InputStream.read` on a supported socket path registers interest with the JDK's poller,56 parks the virtual thread, unmounts, and frees the carrier. Kernel poller threads still57 wait for readiness; the point is that there is no blocked OS thread per socket request.58- Under the hood, socket channels are put in **non-blocking mode** by the JDK and a small59 number of dedicated poller threads (`epoll`/`kqueue`) unpark virtual threads when a file60 descriptor becomes ready. That is an implementation detail, not a specification — do not61 build a design on it, but do use it to explain observations.62- **Many synchronous filesystem paths retain the carrier** in the target HotSpot releases.63 Recognized blocking regions can trigger **compensation**, temporarily expanding the64 scheduler up to `jdk.virtualThreadScheduler.maxPoolSize`. Do not generalize this to every65 provider, native call or memory-mapped access; inspect the path and measure.66- **Capture with compensation is not pinning.** Compensation adds a carrier so throughput67 survives, at the cost of memory and OS threads. Pinning — a native frame or a blocking68 class initialiser — gets no compensation, so it removes a carrier outright. Raising69 `maxPoolSize` helps the first and does nothing for the second.70- The number of platform threads in the scheduler may therefore legitimately exceed71 `availableProcessors()`. Growth towards `maxPoolSize` under operations the scheduler72 recognizes for compensation is the system working as designed; native-frame pinning is73 specifically **not** compensated. Sustained saturation says the compensated-blocking74 workload reached this implementation ceiling, not that raising it is automatically safe.75- **`synchronized` no longer pins** on JDK 24 and later (JEP 491), and `Object.wait` unmounts76 too. On these releases, replacing `synchronized` solely for pinning is unnecessary;77 `-Djdk.tracePinnedThreads` was removed and silently does nothing.78- **Non-blocking I/O is not the reactive model.** A `SocketChannel` in non-blocking mode with79 a `Selector` is non-blocking I/O written imperatively. Reactor and RxJava can compose80 blocking, non-blocking or in-memory work; neither requires Netty. Identify the actual81 transport and execution thread before classifying a pipeline.82- **Both models can use the same kernel facilities on a given transport**, but their83 batching, buffer ownership, syscall cadence and wakeups can differ. Do not infer equal84 performance from a shared `epoll`/`read` foundation; measure CPU, allocation, throughput85 and tail latency. The architectural difference is where suspended logical state lives and86 how it is diagnosed.87- **Blocking inside an event loop is a different severity of bug** from blocking on a pooled88 thread. An event-loop thread serves many connections, so blocking it can stall every89 connection assigned to it. Loop count and assignment are framework/configuration details,90 not a universal `2 × cores`. Combine BlockHound tests with loop-lag and wall-clock evidence.91- A "non-blocking" client library is only non-blocking to the boundary of its own API. A92 reactive database driver that hands work to a bounded internal pool has the same ceiling as93 a JDBC pool, expressed differently.94- Virtual threads make blocking calls **cheap**, not **free**: each in-flight call still95 holds a stack on the heap, a connection, a buffer and any lock it took. The scarce96 resource moved; it did not disappear.97- Readiness-based socket polling does not characterize every `java.nio` API or provider.98 Verify the target implementation before claiming io_uring or completion-based I/O;99 use `io-uring-and-zero-copy` for that investigation.100101## Minimum result102103For each consequential finding, identify the call and execution thread, API/model versus104carrier behavior, supporting evidence and remaining uncertainty. Propose the smallest105change with a resource bound and a before/after check (loop lag, carrier/native memory,106throughput and tail latency as relevant). Do not label an unmeasured optimization a fix.107108## References109110- [What unmounts and what does not](references/what-unmounts.md) — the operation-by-operation111 table, the capture / compensation / pinning distinction with the evidence that separates112 them, handling file-heavy workloads, and verifying a third-party client's behaviour. Read113 when classifying a path or when carrier count is growing.114- [Event loops, pollers and blocking detection](references/event-loops-and-pollers.md) — how115 the JDK's socket poller works and its tuning knobs, the event-loop model and the cost of116 blocking one, BlockHound in tests, and the diagnostics for each model. Read when working on117 or debugging an event-loop-based stack.