All topics
library
advanced

Virtual Threads (Java 21)

Use lightweight virtual threads for high-concurrency I/O workloads without the overhead of platform threads.

A virtual thread is scheduled by the JVM, not the operating system, so you can have millions of them.

Platform threads = taxis (expensive, limited supply). Virtual threads = rideshare (millions of passengers, shared small car fleet that picks up whoever is ready to move).

Key Concepts

1
    Thread.ofVirtual().start(() -> handle(request));
2
    try (var pool = Executors.newVirtualThreadPerTaskExecutor()) {
      for (var task : tasks) pool.submit(task);
    }
3
A platform thread maps 1:1 to an OS thread and costs about a megabyte of stack plus OS scheduling. Ten thousand of them is already painful; a million is impossible.
4
A virtual thread costs a few hundred bytes. When it blocks, the JVM unmounts it from its carrier thread, stores its stack on the heap, and runs something else. No OS thread sits waiting.
5
    platform threads     limited, expensive, block an OS thread
    virtual threads      unlimited in practice, cheap, block only themselves
6
Which is why you can go back to writing blocking code.
    // this scales now
    var user = userApi.fetch(id);          // blocks
    var orders = orderApi.fetch(id);       // blocks
    return new Profile(user, orders);
7
Before Loom, scaling that meant CompletableFuture chains or a reactive framework, with the control flow turned inside out and stack traces to match.
CompletableFuture
8
Do not pool them. A thread pool exists to share something expensive. Virtual threads are cheap, so one per task is the model, and newVirtualThreadPerTaskExecutor is the executor to use.
newVirtualThreadPerTaskExecutor
9
They are for blocking, not for computation. A CPU-bound task occupies its carrier the whole time, so a million of them buys nothing — the parallelism is still your core count. Use the common ForkJoinPool there.
ForkJoinPool
10
Pinning is the limitation to know. A virtual thread blocking inside a synchronized block cannot be unmounted, so it holds its carrier:
synchronized
11
    synchronized (lock) { io.read(); }     // pins the carrier
    lock.lock(); try { io.read(); } ...    // ReentrantLock does not
12
Prefer ReentrantLock over synchronized around blocking I/O. JDK 24 removes most pinning, but the advice still holds on 21.
ReentrantLocksynchronized
13
ThreadLocal works and is worth avoiding at a million threads — a million copies of each value. Scoped values are the intended replacement.
ThreadLocal
14
What the interviewer is probing.1. "What problem do virtual threads solve?" Probing: whether you can state it in terms of cost. Stalls: "They are faster." Moves up: a platform thread costs about a megabyte and an OS scheduling slot, so blocking code could not scale; a virtual thread costs a few hundred bytes and unmounts when it blocks.
15
2. "Should you pool virtual threads?" Probing: the model change. Stalls: "Yes, pools are good practice." Moves up: no — a pool exists to share something expensive, and these are cheap. One per task, via newVirtualThreadPerTaskExecutor.
16
3. "What is pinning?" Probing: the limitation that matters on 21. Stalls: "Nothing I know of." Moves up: a virtual thread blocking inside synchronized cannot unmount and holds its carrier; prefer ReentrantLock around blocking I/O.
17
4. "Do virtual threads speed up CPU-bound work?" Probing: whether you know what they do not fix. Stalls: "Yes, more threads." Moves up: no — parallelism is still bounded by cores; they help when threads are blocked, not when they are computing.