← Back to list

Inside Java Virtual Threads: A Deep Dive into the JVM Implementation. Part 1

Virtual threads make concurrency simple again — giving you the scalability of asynchronous programming with the simplicity of plain…

Nikolay Kudinov · 2025-11-04 09:53 · 0 claps · 3.6 min read
#jdk21 #java #java-internals
Open on Medium ↗
Wiki topics: 💻 · Programming ✨ · Lifestyle · General

Inside Java Virtual Threads: A Deep Dive into the JVM Implementation. Part 1

Virtual threads make concurrency simple again — giving you the scalability of asynchronous programming with the simplicity of plain, readable, synchronous code.

Let’s start with a simple example 👇

    // Launch two tasks in parallel, but the code looks synchronous
    Future<String> serviceA = executor.submit(() -> fetch("Service A"));
    Future<String> serviceB = executor.submit(() -> fetch("Service B"));

    // Wait for both results — simple blocking style
    String result = serviceA.get() + " + " + serviceB.get();

❓ Question 1

Is this a good way to write concurrent code in Java?

💬 Answer

It depends — on whether you’re using virtual threads or not.

  • 🚫 If you use platform threads (e.g. Executors.newFixedThreadPool()): this blocking style doesn’t scale well. Each thread blocks a real OS thread and consumes significant memory. As the number of concurrent tasks grows, so do resource costs.
  • If you use virtual threads (Executors.newVirtualThreadPerTaskExecutor()): this exact same code becomes lightweight and highly scalable.The JVM can park virtual threads when they block, freeing real threads to handle more work — achieving asynchronous scalability without changing your programming style.

🧠 The Big Idea

Virtual threads let you write simple, synchronous code that behaves like efficient asynchronous code under the hood.

You get the best of both worlds — readability and performance, simplicity and scalability.

⚙️ So… How Does It Actually Work?

When a virtual thread blocks (for example, during future.get() ), the JDK decides how to “park” it depending on whether it’s virtual or platform-based:

if (t.isVirtual()) {
    VirtualThreads.park();
} else {
    U.park(false, 0L);
}

This simple-looking conditional hides a lot of JVM magic.

When a virtual thread is parked, it doesn’t actually block a real OS thread.

Instead, the JVM suspends its continuation (essentially a snapshot of its stack) and frees the carrier thread to run something else.

🔍 Under the Hood: The park() Method

From the JDK source code:

 /**
     * Parks until unparked or interrupted. If already unparked then the parking
     * permit is consumed and this method completes immediately (meaning it doesn't
     * yield). It also completes immediately if the interrupt status is set.
     */
    @Override
    void park() {
        assert Thread.currentThread() == this;

        // complete immediately if parking permit available or interrupted
        if (getAndSetParkPermit(false) || interrupted)
            return;

        // park the thread
        boolean yielded = false;
        setState(PARKING);
        try {
            yielded = yieldContinuation();  // may throw
        } finally {
            assert (Thread.currentThread() == this) && (yielded == (state() == RUNNING));
            if (!yielded) {
                assert state() == PARKING;
                setState(RUNNING);
            }
        }

        // park on the carrier thread when pinned
        if (!yielded) { 
            parkOnCarrierThread(false, 0);
        }
    }

The Key Operation — yieldContinuation()

   /**
     * Unmounts this virtual thread, invokes Continuation.yield, and re-mounts the
     * thread when continued. When enabled, JVMTI must be notified from this method.
     * @return true if the yield was successful
     */
    @Hidden
    @ChangesCurrentThread
    private boolean yieldContinuation() {
        // unmount
        notifyJvmtiUnmount(/*hide*/true);
        unmount();
        try {
            return Continuation.yield(VTHREAD_SCOPE);
        } finally {
            // re-mount
            mount();
            notifyJvmtiMount(/*hide*/false);
        }
    }

Let’s translate that into plain English 👇

  1. Unmount the virtual thread from its carrier (real OS thread).
  2. Save its current stack into a heap-allocated continuation (pause point).
  3. Free the carrier thread to do other work.
  4. When the virtual thread is unparked, remount it and continue from the exact point where it paused.

⚡ How Java Virtual Threads Handle I/O Blocking

Let’s revisit our question — but this time for I/O.

❓ Question 2

Is this code still “good” when it involves blocking I/O operations like SocketChannel.read()?

💬 The Answer

Again, it depends on the thread type.

When a virtual thread performs a blocking I/O call — for example, SocketChannel.read() — it does not block a real OS thread.

Instead:

1️⃣ The JVM detects the blocking I/O call.

2️⃣ The virtual thread unmounts from its carrier thread.

3️⃣ Its Java stack is saved in a continuation.

4️⃣ The carrier thread is freed for other work.

5️⃣ When the I/O operation completes, the virtual thread resumes from the exact point it paused.

This required deep integration in the JDK, especially in classes like SocketChannelImpl, to make traditional blocking I/O work seamlessly with virtual threads.

Internals: How the JDK Does It:

// Read, no timeout
configureSocketNonBlockingIfVirtualThread();
n = tryRead(b, off, len);
while (IOStatus.okayToRetry(n) && isOpen()) {
    park(Net.POLLIN);
    n = tryRead(b, off, len);
}

Here’s what happens inside the park() implementation:

  default void park(int event, long nanos) throws IOException {
        if (Thread.currentThread().isVirtual()) {
            Poller.poll(getFDVal(), event, nanos, this::isOpen);
        } else {
            long millis;
            if (nanos <= 0) {
                millis = -1;
            } else {
                millis = NANOSECONDS.toMillis(nanos);
                if (nanos > MILLISECONDS.toNanos(millis)) {
                    // Round up any excess nanos to the nearest millisecond to
                    // avoid parking for less than requested.
                    millis++;
                }
            }
            Net.poll(getFD(), event, millis);
        }
    }

Notice how virtual threads take a different code path — their blocking is managed cooperatively via the poller rather than by locking a native thread.

🧵 Conclusion

Java Virtual Threads represent one of the most significant evolutions in JVM history.

They bring lightweight concurrency to the platform while preserving Java’s familiar synchronous style.

Key takeaways:

  1. Elegant mount/unmount mechanism for switching thread context.
  2. Continuation-based system for saving and restoring execution state.
  3. Smart blocking operation detection in the JDK.
  4. A dedicated scheduler optimized for virtual workloads.

This isn’t just a new API — it’s a rethinking of concurrency at the JVM level.

Understanding how it works helps developers appreciate the sophisticated machinery making our concurrent Java code both simpler and faster.

🧩 Coming Up in Part 2:

We’ll explore how continuations are implemented, and how you can tune your applications to make the most of this groundbreaking feature.


메타데이터
post_id
0cf6793ac3bd
slug
inside-java-virtual-threads-a-deep-dive-into-the-jvm-implementation-part-1-0cf6793ac3bd
url
https://medium.com/@nikolaykudinov/inside-java-virtual-threads-a-deep-dive-into-the-jvm-implementation-part-1-0cf6793ac3bd
canonical_url
https://medium.com/@nikolaykudinov/inside-java-virtual-threads-a-deep-dive-into-the-jvm-implementation-part-1-0cf6793ac3bd
author_url
https://medium.com/@nikolaykudinov
status
ok
fetched_at
2026-06-25 07:00:49