Platform Threads vs Virtual Threads
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 thread | Virtual thread | |
|---|---|---|
| Backed by | One OS thread for its whole life | A carrier thread only while running |
| Stack | Fixed, reserved up front (often about 1 MB) | Small, grows in the heap as needed |
| Creation cost | High (an OS call) | Very low |
| Practical number | Thousands | Millions |
| Scheduled by | The operating system | The JVM (a ForkJoinPool) |
| When it blocks | Its OS thread sits idle | It unmounts; the carrier runs other work |
| Daemon | Configurable | Always daemon |
| Priority | Configurable | Always normal; setPriority has no effect |
| Pooling | Recommended | Not recommended |
| Best for | CPU-heavy work, long-lived background threads | Many 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.
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));
}
}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 18Explanation:
- 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
- Easy: Change
REQUESTSto 1,000 and run the checkpoint again. How does the gap between the pool and virtual threads change? - Harder: Replace
Thread.sleep(100)with a CPU-heavy loop that takes about 100 milliseconds, and explain the new results.
Key Takeaways
Was this tutorial helpful?