Virtual Threads in Java
Virtual threads, added as a final feature in Java 21, are lightweight threads managed by the JVM rather than by the operating system. A normal (platform) thread is a wrapper around an expensive operating-system thread, so a program can only afford a few thousand of them. A virtual thread costs only a small amount of memory, so a program can create hundreds of thousands or millions. They are designed for code that spends most of its time waiting, such as web servers calling databases and other services. In this lesson, you will learn how to create virtual threads and how they work.
How Virtual Threads Work
- Virtual threads run on a small pool of platform threads called carrier threads, by default one per CPU core.
- When a virtual thread blocks (sleep, a socket read, a lock wait), the JVM unmounts it from its carrier and stores its stack in the heap. The carrier immediately runs another virtual thread.
- When the blocking operation finishes, the virtual thread is mounted again on any free carrier and continues.
- Your code stays plain and blocking: no callbacks or
CompletableFuturechains are needed.
Ways to Create Virtual Threads
| Way | Code |
|---|---|
| Start one directly | Thread.startVirtualThread(task) |
| Thread builder | Thread.ofVirtual().name("worker").start(task) |
| One per task in an executor | Executors.newVirtualThreadPerTaskExecutor() |
| A factory for other APIs | Thread.ofVirtual().factory() |
Example: Creating Virtual Threads
Let's see an example of the three common ways.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class VirtualBasics {
public static void main(String[] args) throws Exception {
Thread t1 = Thread.startVirtualThread(() ->
System.out.println("Hello from a virtual thread"));
t1.join();
System.out.println("t1 is virtual? " + t1.isVirtual());
Thread t2 = Thread.ofVirtual().name("navika-task").start(() ->
System.out.println("Running " + Thread.currentThread().getName()));
t2.join();
try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
Future<String> f = ex.submit(() -> {
Thread.sleep(100);
return "virtual? " + Thread.currentThread().isVirtual();
});
System.out.println("Executor task says " + f.get());
}
System.out.println("main is virtual? "
+ Thread.currentThread().isVirtual());
}
}Hello from a virtual thread
t1 is virtual? true
Running navika-task
Executor task says virtual? true
main is virtual? falseExplanation: startVirtualThread creates and starts a virtual thread in one call. The builder Thread.ofVirtual() lets you name it. newVirtualThreadPerTaskExecutor() creates a new virtual thread for every submitted task, so existing executor code switches to virtual threads with a one-line change. The main thread itself is a platform thread.
Example: 100,000 Sleeping Threads
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class ManyThreads {
public static void main(String[] args) {
AtomicInteger done = new AtomicInteger();
long start = System.nanoTime();
try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
ex.submit(() -> {
Thread.sleep(1000);
return done.incrementAndGet();
});
}
}
long ms = (System.nanoTime() - start) / 1_000_000;
System.out.println("Finished tasks: " + done.get());
System.out.println("All in about one second (under 3 s)? "
+ (ms < 3000));
}
}Finished tasks: 100000
All in about one second (under 3 s)? trueExplanation: 100,000 tasks each sleep for one second. With virtual threads, they all sleep at the same time on a handful of carrier threads, so the whole job takes a little over one second. Creating 100,000 platform threads would use gigabytes of stack memory or fail outright.
Common Mistakes
Pooling virtual threads. They're cheap; create a new one per task (see "Virtual Thread Best Practices").
Expecting speed-ups for CPU-bound code. The number of cores is still the limit.
Assuming Thread.currentThread() is a platform thread. In a virtual thread it returns the virtual thread.
Interview Corner
What are virtual threads?
Virtual threads, final in Java 21 (JEP 444), are Thread instances implemented by the JVM rather than mapped one-to-one onto operating-system threads. They're scheduled onto a small pool of carrier platform threads; when a virtual thread blocks on I/O, sleep or a lock, it's unmounted and its stack saved in the heap, freeing the carrier. They're cheap enough to create one per task, which allows simple blocking code to scale to very high concurrency for I/O-bound workloads.
Practice
- Easy: Start ten virtual threads named
vt-1tovt-10withThread.ofVirtual().name("vt-", 1), and print each name. - Harder: Change
ManyThreadsto useExecutors.newFixedThreadPool(200)and predict, then measure, how long it takes.
Key Takeaways
Was this tutorial helpful?