Platform Threads vs Virtual Threads

Naveen AnamaneniUpdated · 5 min read

Java now has two kinds of threads. A platform thread is the classic Thread: a thin wrapper around an operating-system thread, with a large fixed stack, scheduled by the operating system. A virtual thread is a Thread managed by the JVM: it has a small, growable stack stored in the heap and runs on top of a few platform "carrier" threads. Both use the same Thread API, so the same code can run on either. In this lesson, you will compare them and then measure the difference on 10,000 simulated requests.

Differences Between Platform and Virtual Threads

Platform threadVirtual thread
Backed byOne OS thread for its whole lifeA carrier thread only while running
StackFixed, reserved up front (often about 1 MB)Small, grows in the heap as needed
Creation costHigh (an OS call)Very low
Practical numberThousandsMillions
Scheduled byThe operating systemThe JVM (a ForkJoinPool)
When it blocksIts OS thread sits idleIt unmounts; the carrier runs other work
DaemonConfigurableAlways daemon
PriorityConfigurableAlways normal; setPriority has no effect
PoolingRecommendedNot recommended
Best forCPU-heavy work, long-lived background threadsMany concurrent tasks that wait on I/O

The Checkpoint: 10,000 Simulated Requests

This program handles 10,000 "requests", each of which waits 100 milliseconds as if calling a database, in three ways: a pool of 200 platform threads, one new platform thread per request, and one virtual thread per request. It reports the time taken and the number of platform (OS) threads alive while the tasks ran. Results depend on the computer, so the output is a sample.

RequestLoad.java▶ Try it
import java.lang.management.ManagementFactory;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
 
public class RequestLoad {
    static final int REQUESTS = 10_000;
 
    static String run(String label, Supplier<ExecutorService> factory) {
        AtomicInteger peak = new AtomicInteger();
        var threads = ManagementFactory.getThreadMXBean();
        long start = System.nanoTime();
        try (ExecutorService ex = factory.get()) {
            for (int i = 0; i < REQUESTS; i++) {
                ex.submit(() -> {
                    Thread.sleep(100);
                    peak.accumulateAndGet(threads.getThreadCount(), Math::max);
                    return null;
                });
            }
        }
        long ms = (System.nanoTime() - start) / 1_000_000;
        return label + ": " + ms + " ms, OS threads seen " + peak.get();
    }
 
    public static void main(String[] args) {
        System.out.println(run("Pool of 200 platform threads",
            () -> Executors.newFixedThreadPool(200)));
        System.out.println(run("Platform thread per request ",
            () -> Executors.newThreadPerTaskExecutor(
                      Thread.ofPlatform().factory())));
        System.out.println(run("Virtual thread per request  ",
            Executors::newVirtualThreadPerTaskExecutor));
    }
}
Output
Pool of 200 platform threads: 5209 ms, OS threads seen 206
Platform thread per request : 630 ms, OS threads seen 2708
Virtual thread per request  : 152 ms, OS threads seen 18

Explanation:

  • Pool of 200: only 200 requests can wait at once, so 10,000 requests need 50 rounds of 100 milliseconds: about 5 seconds.
  • Platform thread per request: all requests can wait at once, but creating thousands of OS threads is slow and memory-hungry; at one point over 2,700 OS threads were alive.
  • Virtual threads: every request waits at once on about a dozen carrier threads (plus the JVM's own threads), finishing in about 150 milliseconds while using the fewest OS threads.

getThreadCount() reports only platform threads, which is exactly the point: the virtual threads didn't need their own OS threads.

Common Mistakes

Comparing them on CPU-heavy work. Both are limited by the number of cores there.

Reading getThreadCount() as the number of virtual threads. It counts platform threads only.

Setting priorities or daemon status on virtual threads. They're always normal priority and daemon.

Interview Corner

What is the difference between platform threads and virtual threads?

A platform thread is a java.lang.Thread mapped one-to-one onto an operating-system thread for its entire life; it has a large fixed stack, is scheduled by the OS, and is expensive to create, so applications pool a limited number. A virtual thread is a Thread scheduled by the JVM onto a small set of carrier platform threads; its stack lives in the heap and grows as needed, and when it blocks it unmounts from its carrier. Virtual threads are cheap enough to create per task and suit high-concurrency I/O-bound workloads; platform threads remain appropriate for CPU-bound work.

Practice

  1. Easy: Change REQUESTS to 1,000 and run the checkpoint again. How does the gap between the pool and virtual threads change?
  2. Harder: Replace Thread.sleep(100) with a CPU-heavy loop that takes about 100 milliseconds, and explain the new results.

Quick check: Platform vs virtual threads

4 questions
1Which thread is mapped one-to-one onto an OS thread?
2Where is a virtual thread's stack stored while it's waiting?
3Should virtual threads be pooled?
4Why did the pool of 200 take about 5 seconds for 10,000 requests?

Key Takeaways

Was this tutorial helpful?

Lesson 429 of 482