Why architecture matters here

Virtual threads architecture matters because Java's thread model was the biggest scale limit for many workloads. Each platform thread was ~2 MB of stack; you could not comfortably run more than a few thousand. To handle 100k connections, you had to use NIO and reactive frameworks with their callback complexity. Virtual threads remove the choice: you can write straight-line blocking code and scale to millions.

Cost is real infrastructure savings. A service that needed 10 pods for 10k concurrent requests with platform threads may need 1 pod with virtual threads. Ops costs drop; developer productivity improves.

Reliability comes from simpler code. Reactive frameworks work but are hard to debug (stack traces are useless), hard to test, and require special libraries end-to-end. Virtual threads let you write ordinary Java that scales.

Advertisement

The architecture: every piece explained

Walk the diagram top to bottom.

Application code. Uses Thread.startVirtualThread(runnable) or ExecutorService with newVirtualThreadPerTaskExecutor. Or Thread.ofVirtual().name("worker").start().

Virtual Thread. A JVM-managed thread that isn't backed by an OS thread. Cheap: ~200 bytes for basic state. You can create millions.

Continuation. The state snapshot that lets a virtual thread be parked (suspended) and resumed. Java's implementation of coroutine-style semantics.

ForkJoin Pool (Carriers). A small pool of platform OS threads (default = CPU count) that actually execute virtual threads. Each carrier runs one virtual thread at a time.

Scheduler. Mounts a virtual thread on a carrier to run; unmounts (parks) when the virtual thread blocks; picks a different virtual thread to mount. Efficient work-stealing.

Blocking I/O. The killer feature. When a virtual thread calls a blocking operation like socketRead, the runtime yields the carrier back to the scheduler. Another virtual thread runs. When I/O completes, the original virtual thread is remounted.

Synchronized (pins). Legacy pain point. Inside synchronized blocks, virtual threads pin the carrier — carrier can't be reused while the virtual thread is inside. Long synchronized blocks lose the virtual thread advantage.

ReentrantLock. Modern lock. Yields carrier on contention. Recommended over synchronized for virtual thread compatibility.

Structured Concurrency. Task scopes (StructuredTaskScope, preview) that group related tasks with lifetimes tied to the scope. Prevents thread leaks and simplifies cancellation.

Scoped Values. Successor to ThreadLocal. Immutable, scoped to a call tree. Suitable for virtual threads (ThreadLocal has memory issues at millions of threads).

Application codeThread.startVirtualThreadVirtual Threadlightweight JVM threadContinuationstate snapshotForkJoin Pool (carriers)small pool of OS threadsSchedulermounts + parks virtual threadsBlocking I/Oyields carrierSynchronized (pins)carrier heldReentrantLockfriendly to VTStructured Concurrencytask scopesScoped Valuesimmutable per-thread contextAvailable Java 21+, GA. Great for I/O bound workloads.
Java Virtual Threads: application creates cheap VTs, scheduler mounts on carrier OS threads, blocking I/O yields the carrier, synchronized pins it; VTs shine on I/O-bound workloads.
Advertisement

End-to-end request handling

Trace a request. HTTP server accepts a connection. Each request spawns a virtual thread: Thread.startVirtualThread(() -> handle(request)).

handle() reads a DB row (blocking JDBC call). The virtual thread's continuation is saved; the carrier is released to run another virtual thread. When JDBC returns data, the virtual thread is remounted on some carrier and continues.

The request handler calls three microservices in parallel via HttpClient. With structured concurrency: try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { scope.fork(() -> callA()); scope.fork(() -> callB()); scope.fork(() -> callC()); scope.join(); }.

Each fork creates a child virtual thread. All three block on HTTP; carriers freely serve other requests. When all three complete, the scope joins.

The handler assembles the response and returns. Virtual thread ends; JVM reclaims the ~200 bytes.

Meanwhile the JVM is running with 4 carrier threads (on a 4-core machine) and 50,000 concurrent virtual threads for 50,000 in-flight requests. Under platform threads, this would be 50k×2 MB = 100 GB. Under virtual threads it's a few tens of MB.