Skip to content
  • Virtual Threads (Java 21) · Java

Virtual Threads (Java 21)

LearnViaNotes @learnvianotes· 4 min read

Virtual threads (JEP 444, final in Java 21) are lightweight threads managed by the JVM — you can have millions, so plain blocking code scales like async code.

Platform vs virtual threads

Platform threadVirtual thread
Backed byOne OS thread for its whole lifeAny carrier thread, only while running
StackFixed, ~1 MB reservedHeap-allocated, grows as needed (KBs)
How manyThousandsMillions
CreationExpensive — pool themCheap — create one per task, never pool
SchedulerOSJVM (ForkJoinPool of carriers, ~one per core)
Always daemon?ConfigurableYes; priority fixed
Best forCPU-bound work, long-lived threadsHigh-concurrency blocking I/O (servers, clients)

Creating virtual threads

Java
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread v = Thread.ofVirtual().name("v-worker").start(() ->
            System.out.println(Thread.currentThread().getName()
                + " virtual=" + Thread.currentThread().isVirtual()
                + " daemon=" + Thread.currentThread().isDaemon()));
        v.join();

        Thread quick = Thread.startVirtualThread(() -> System.out.println("started with startVirtualThread"));
        quick.join();

        Thread unstarted = Thread.ofVirtual().unstarted(() -> { });
        System.out.println("state = " + unstarted.getState());

        Thread p = Thread.ofPlatform().name("p-worker").start(() ->
            System.out.println(Thread.currentThread().getName() + " virtual=" + Thread.currentThread().isVirtual()));
        p.join();
    }
}
Output
v-worker virtual=true daemon=true
started with startVirtualThread
state = NEW
p-worker virtual=false

One virtual thread per task

10 000 tasks that each block for 10 ms. A pool of 100 platform threads would need ~1 s; with a thread per task the sleeps all overlap and the whole batch finishes in roughly the time of one.

Java
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Main {
    public static void main(String[] args) throws Exception {
        List<Future<Integer>> results = new ArrayList<>();
        try (ExecutorService exec = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i < 10_000; i++) {
                int n = i;
                results.add(exec.submit(() -> {
                    Thread.sleep(10);          // blocking call: the virtual thread unmounts
                    return n;
                }));
            }
        }                                      // close() waits for all 10 000
        long sum = 0;
        for (Future<Integer> f : results) sum += f.resultNow();
        System.out.println(results.size() + " tasks, sum = " + sum);
    }
}
Output
10000 tasks, sum = 49995000

When they help — and when they don't

Pitfalls

PitfallWhyInstead
Pooling virtual threadsThey are cheap; pooling just limits concurrencyOne per task; limit access to a scarce resource with a Semaphore
Pinning (Java 21)Blocking inside synchronized (or a native call) keeps the carrier occupiedUse ReentrantLock around blocking I/O; diagnose with -Djdk.tracePinnedThreads=full (fixed for synchronized in JDK 24, JEP 491)
Heavy ThreadLocal cachesA million threads × a cached object each = memory blow-upPass context explicitly; scoped values (preview)
Long CPU loopsNo time-slicing preemption between virtual threadsKeep CPU-heavy work on a platform pool

Do virtual threads make code faster?

What is a carrier thread? What is pinning?

Should you use a thread pool of virtual threads?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report