Blocking & Unmounting: The Magic of Virtual Threads
In the previous post, we explored the M:N scheduling model where many virtual threads map to a few carrier threads. But what happens when a virtual thread needs to wait? In the platform thread world, waiting meant holding onto the OS thread, effectively wasting a precious resource. In the virtual thread world, we "unmount."
The Blocking Problem
Traditional I/O operations (reading from a socket, querying a database, reading a file) are blocking. While the CPU waits for the data to arrive, the thread sits idle. If that thread is an OS thread (Platform Thread), we are tying up 1-2MB of memory and a slot in the kernel scheduler for doing absolutely nothing.
The Unmounting Solution
When a Virtual Thread calls a blocking I/O method (like InputStream.read()), the JVM runtime
intercepts this call. Instead of blocking the underlying OS thread (the Carrier Thread), the Virtual Thread
yields its execution.
// This looks like blocking code...
try (var socket = new Socket("example.com", 80)) {
// ...but under the hood, it unmounts!
int data = socket.getInputStream().read();
}
Here is the sequence of events:
- The Virtual Thread initiates the I/O operation.
- The JVM registers a non-blocking handler for that file descriptor in the OS (using mechanisms like epoll/kqueue/IOCP).
- The Virtual Thread saves its stack frames and state to the heap.
- The Virtual Thread unmounts from the Carrier Thread.
- The Carrier Thread is now free! It pops the next ready Virtual Thread from the run queue and starts executing it.
The IO Wait Room
Where do the unmounted threads go? They sit in a "Wait" state (effectively stored in heap memory) until the OS signals that the I/O operation is complete. Once the data is ready, the JVM:
- Receives the signal from the OS.
- Identifies which Virtual Thread was waiting for this data.
- Reschedules the Virtual Thread (adds it back to the run queue).
- Eventually, a Carrier Thread (not necessarily the same one!) picks it up ("mounts" it) and resumes execution right where it left off.
Visualization: The IO Wait Room
The visualization below demonstrates this flow. Virtual Threads (green circles) enter the CPU (Carrier Threads). When they hit a blocking operation, they move to the "IO Wait Room" (purple area), leaving the Carrier Thread free to process other tasks. When I/O completes, they return to the Queue to be executed again.
Why This Matters
This mechanism allows Java applications to handle concurrent connections scale into the millions. We are no longer limited by the number of OS threads we can spawn. We are limited only by available heap memory and network sockets. The "Thread-per-Request" style of programming is back, but this time, it's efficient.