Virtual threads

Read the explanation
What if

Three virtual threads share two real (carrier) threads. When one blocks, it steps off its carrier so another can run.

0123456executor.close()continuescarrier-1: vt-1 computevt-1carrier-1: vt-3 computevt-3carrier-1: vt-2 computevt-2carrier-1: vt-3 computevt-3carrier-2: vt-2 computevt-2carrier-2: vt-1 computevt-1

Tap any block to see what that thread is doing.

  • Waiting / parked
  • Running
Step 1/6

Three virtual threads, two carriers. vt-1 and vt-2 mount; vt-3 waits for a free carrier.

1ThreadFactory vts = Thread.ofVirtual().name("vt-", 1).factory();2try (var executor = Executors.newThreadPerTaskExecutor(vts)) {3    for (int i = 0; i < 3; i++) {4        executor.submit(() -> {5            compute();            // runs on a carrier thread6            Thread.sleep(200);    // blocks: the virtual thread unmounts7            compute();            // remounts, maybe on another carrier8            return null;9        });10    }11} // close() waits for every task

Explanation

What just happened

Three virtual threads ran on two real OS threads, called carriers. A virtual thread only occupies a carrier while it’s actually running. When vt-1 and vt-2 called sleep(), they unmounted, and carrier-1 picked up vt-3. After the sleeps, vt-1 and vt-2 remounted, on swapped carriers. For a moment all three were asleep and both carriers were free.

Try the what-if switch

2 platform threads: the same three tasks on newFixedThreadPool(2). A sleeping task keeps its OS thread asleep with it, so task 3 can’t start until task 1 is completely done. The work finishes at 8 instead of 5.

Why this matters

A platform thread that blocks on I/O holds an OS thread doing nothing. That’s why servers needed thread pools and callbacks. A virtual thread that blocks costs almost nothing, so you can write simple blocking code and still run thousands of tasks at once.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (var request : requests) executor.submit(() -> handle(request));
}

Good to know

Common questions

What is a carrier thread?

A platform (OS) thread that runs virtual threads. A virtual thread is mounted on a carrier only while it runs. When it blocks, it unmounts, and may resume later on a different carrier.

What is the difference between virtual threads and platform threads?

A blocked platform thread holds its OS thread doing nothing. A blocked virtual thread frees its carrier for other work, so a few carriers can serve thousands of waiting tasks.

Does synchronized pin virtual threads?

Before JDK 24, yes: blocking inside synchronized kept the virtual thread stuck to its carrier. JDK 24 removed that (JEP 491). Threadline's tests check both on JDK 21 and JDK 25.

Should I pool virtual threads?

No. They're cheap, so create one per task with Executors.newVirtualThreadPerTaskExecutor(). To limit load on a database or API, use a Semaphore.

Verified: this timeline matches a real program, checked in CI on JDK 21 and 25. How we verify. Unclear or wrong? Tell us