blocking-and-nonblocking-io · git:20261016.c769bd8 · 2026-10-16 · sha256 0855b59117f96331
blocking-and-nonblocking-io git:20261016.c769bd8A
Immutable. This exact content never changes and is served at /api/v1/blob/0855b59117f96331.
--- name: blocking-and-nonblocking-io description: > 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). --- # Blocking and Non-Blocking I/O ## Purpose Keep four distinct properties distinct when diagnosing or reviewing an I/O path: ```text Blocking API the caller may wait for this call's specified result Blocked OS thread a kernel-schedulable entity is parked and unavailable Non-blocking I/O no wait for readiness; partial progress or would-block is possible Asynchronous model completion is represented separately, e.g. a stage or callback ``` A virtual thread reading a supported socket path without an enclosing pinning frame 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. Define what completion means before releasing resources: a read may return fewer bytes than requested, and a streaming response may return before its body is consumed. Readiness, API return, transfer completion and a remote business effect are different milestones; none alone proves the others. The classification reference gives a concrete streaming example. ## 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, effective VM flags (including locking mode), 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 1. **Establish which property matters.** "Is this blocking?" is four questions. Inspect the call site, configuration, tests and reported symptom first; ask only for missing context that changes the classification or remedy. A review request need not change code. Follow the affected path from its caller through submission/subscription, actual I/O, callbacks and consumption/cleanup. Read the selected implementation and scheduler wiring; an offloaded entry call does not establish where later body reads or fallback calls run. 2. **Classify each I/O wait on the path**: can it unmount? If it retains the carrier, does the implementation request compensation, or does it block without compensation? Identify any pinning frame separately; the actual path determines the cost and fix. 3. **Check for file system I/O.** Identify the concrete synchronous path and whether the JDK marks it for compensation. For an asynchronous API, trace submission, worker I/O and completion separately; an immediate return says nothing about the worker's syscall. 4. **Check for foreign code**: JNI, FFM, a driver with a native transport. An enclosing native frame prevents unmounting. Pinning alone does not request compensation; check whether the actual blocking operation has a separate compensation hook. 5. **If an event loop is involved, find every blocking call inside it** — one is enough to stall every connection that loop serves. 6. **Match the evidence to the claim.** Version-matched contracts/source and known execution context can establish a classification or a misplaced blocking call. Test the call's actual thread when practical; measure occurrence and cost with loop lag, carrier counts, `jdk.VirtualThreadPinned` and wall-clock profiles as appropriate. Missing events do not rule out native blocking. If the selected path or context is unknown, keep that conclusion conditional and identify the artifact or capture that would resolve it. New evidence should revise the affected diagnosis and remedy, not leave a disproved explanation in place. ## 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, supported socket paths use **non-blocking mode** and shared readiness pollers. OS mechanisms and platform/virtual poller-thread arrangements vary by JDK and provider; see the poller reference. 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 under an expansion policy initialized by `jdk.virtualThreadScheduler.maxPoolSize`. Do not generalize this to every provider, native call or memory-mapped access; inspect the path and measure. - **Carrier retention and compensation are separate questions.** Recognized blocking regions can request spare carriers, at the cost of memory and OS threads. Pinning — for example, a native frame preventing a Java park from unmounting — does not itself request compensation. A native operation explicitly wrapped in a JDK compensation region can still be compensated. Raising `maxPoolSize` only creates possible compensation headroom; it cannot make an otherwise uncompensated wait trigger expansion. - 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; pinning alone does not trigger that growth. Sustained saturation says the compensated-blocking workload reached this implementation ceiling, not that raising it is automatically safe. - **With default HotSpot locking, `synchronized` no longer pins** on JDK 24+ (JEP 491), and `Object.wait` unmounts too. Legacy `-XX:LockingMode=1` still permits monitor pinning in HotSpot 24–25; inspect effective flags and why an override exists before replacing locks or changing configuration. With default locking, replacing `synchronized` solely for pinning is unnecessary. `-Djdk.tracePinnedThreads` was removed in JDK 24 and silently does nothing; use the release-appropriate evidence in the classification reference. - **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 compatible BlockHound tests, where available, 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 a worker/queue ceiling distinct from connection limits and database capacity; inspect each bound. - **An asynchronous result does not end resource ownership.** Keep buffers and admission permits owned until the underlying operation is finished; a timeout or cancelled Future may only end the caller's wait. For asynchronous file channels, use the reference's executor and cancellation contracts before choosing bounds or reusing buffers. - Unmounting makes thread waiting **cheap**, not **free**: in-flight calls retain stack state and any resources they still own, such as connections, buffers or locks. Bound admission as well as active I/O; a semaphore's waiters can still retain unbounded state. - 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. Retain an adequate path, or 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. For a service-wide choice between reactive and thread-per-request, pass the classified paths, runtime/framework versions, resource limits and latency evidence to `reactive-and-virtual-thread-selection`; request a model decision with migration costs. If unavailable, present these constraints and viable alternatives without prescribing a rewrite. For carrier-mechanics diagnosis beyond classification, pass stacks, recording settings and version details to `virtual-threads-internals`; without it, report the remaining hypothesis and next discriminating capture. ## References - [What unmounts and what does not](references/what-unmounts.md) — the operation-by-operation table, the capture / compensation / pinning distinction with the evidence that separates them, synchronous/asynchronous file work, completion ownership, and verifying a third-party client's behaviour. Read when classifying a path, choosing an async-file executor, handling cancellation, or when carrier count is growing. - [Event loops, pollers and blocking detection](references/event-loops-and-pollers.md) — 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.