Java 21 Virtual Threads Tutorial with Examples
Introduction
A typical Java server handles each request on its own thread - simple to write, read and debug. But every Java thread used to be a platform thread: a thin wrapper around an operating system thread, which takes around a megabyte of memory and is expensive to create and switch. A server can only run a few thousand of them, and since most request-handling time is spent waiting - for a database, another service or a file - those threads sit idle, and the server runs out of threads long before it runs out of CPU. The usual workaround, asynchronous and reactive programming, scales well but makes code much harder to write and debug. Virtual threads, previewed in Java 19 and 20 and final in Java 21Click to read tutorial: Overview of New Features in Java 21 (JEP 444), solve this differently: they are lightweight threads managed by the JVM, so an application can have millions of them. This tutorial explains virtual threads with examples - creating them, running thousands of blocking tasks, how they work, pinning, and when to use them.
Creating virtual threads
A virtual thread is still a
OUTPUT of the above code
Ten thousand blocking tasks
The real power of virtual threads shows with many tasks which mostly wait. The program below runs 10,000 tasks which each sleep for one second - standing in for a call to a database or a remote service - first on a pool of 100 platform threads, and then with one virtual thread per task -
OUTPUT of the above code
Explanation of the code
Most of the JDK's blocking operations were rewritten to work this way - which is why the socket implementation was rewritten in Java 13Click to read: socket implementation was rewritten in Java 13, among other preparations.
Pinning and other caveats
java.lang.Thread, so all the existing APIs work. Java 21 added builders to create them -
Three ways to start a virtual thread
//CreateVirtualThreads.java
public class CreateVirtualThreads {
public static void main(String[] args) throws InterruptedException {
Thread t1 = Thread.startVirtualThread(() -> System.out.println("Hello from a virtual thread"));
Thread t2 = Thread.ofVirtual().name("worker-1").start(() ->
System.out.println("Running in " + Thread.currentThread().getName()
+ ", isVirtual = " + Thread.currentThread().isVirtual()));
Thread t3 = Thread.ofPlatform().name("platform-1").start(() ->
System.out.println("Running in " + Thread.currentThread().getName()
+ ", isVirtual = " + Thread.currentThread().isVirtual()));
t1.join(); t2.join(); t3.join();
System.out.println("Virtual threads are daemon threads: " + t1.isDaemon());
}
}
Hello from a virtual thread Running in worker-1, isVirtual = true Running in platform-1, isVirtual = false Virtual threads are daemon threads: true
Thread.ofVirtual() and Thread.ofPlatform() return builders for the two kinds of thread, and isVirtual() tells them apart. Virtual threads are always daemon threads, so the JVM does not wait for them on exit - which is why the program joins them.
Platform threads vs virtual threads for blocking tasks
//VirtualThreadsScale.java
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
public class VirtualThreadsScale {
static void runTasks(String label, ExecutorService executor, int tasks) {
long start = System.nanoTime();
try (executor) { // close() waits for all tasks (Java 19+)
IntStream.range(0, tasks).forEach(i -> executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // a blocking call, e.g. a database query
return i;
}));
}
System.out.printf("%-34s %,6d tasks in about %d s%n", label, tasks, Math.round((System.nanoTime() - start) / 1e9));
}
public static void main(String[] args) {
runTasks("Fixed pool of 100 platform threads", Executors.newFixedThreadPool(100), 1_000);
runTasks("One virtual thread per task", Executors.newVirtualThreadPerTaskExecutor(), 10_000);
}
}
Fixed pool of 100 platform threads 1,000 tasks in about 10 s One virtual thread per task 10,000 tasks in about 1 s
- With 100 platform threads, only 100 tasks can wait at a time, so even one tenth of the work - 1,000 tasks - takes 10 rounds of one second. 10,000 tasks would take 100 seconds.
Executors.newVirtualThreadPerTaskExecutor()starts a new virtual thread for every task. All 10,000 tasks sleep at the same time, so they finish in about one second, using only a few operating system threads.- Virtual threads are cheap enough that they should not be pooled: create a new one for every task, and use a semaphore if you need to limit how many tasks use a scarce resource at once.
- The code inside the tasks is ordinary blocking code - no callbacks, no
CompletableFuturechains. That is the point: virtual threads give the scalability of asynchronous code with the simplicity of synchronous code.
ForkJoinPool with one carrier per CPU core. When a virtual thread performs a blocking operation, such as reading from a socket, sleeping, or waiting on a lock from java.util.concurrent, the JVM unmounts it: its stack is saved to the heap, and the carrier thread is free to run another virtual thread. When the operation completes, the virtual thread is mounted again on a carrier, possibly a different one, and continues. Because a virtual thread's stack lives in the heap and grows as needed, it uses only a few hundred bytes to a few kilobytes of memory, instead of a megabyte.Most of the JDK's blocking operations were rewritten to work this way - which is why the socket implementation was rewritten in Java 13Click to read: socket implementation was rewritten in Java 13, among other preparations.
- Pinning: in Java 21, a virtual thread which blocks while inside a
synchronizedblock or method, or in a native method, cannot be unmounted - it stays pinned to its carrier, blocking the carrier too. With few carriers, heavy pinning can limit scalability. The workaround in Java 21 was to useReentrantLockinstead ofsynchronizedaround blocking calls. Java 24 removed pinning for synchronizedClick to read: Java 24 removed pinning for synchronized. - CPU-bound work does not benefit: virtual threads help when tasks wait. For computations, the number of CPU cores is the limit, whatever kind of thread is used.
- ThreadLocal: thread-local variables work, but with millions of threads, large per-thread caches in
ThreadLocals can use a lot of memory. Scoped valuesClick to read: Scoped values, final in Java 25, are designed for this world. - Tools: thread dumps with
jcmd <pid> Thread.dump_to_file -format=jsoninclude virtual threads, and JFR has events for them.
spring.threads.virtual.enabled=true, and servers such as Tomcat and Jetty can run each request on a virtual thread.
Summary
In this tutorial we saw how virtual threads, final in Java 21, are lightweight threads managed by the JVM. We created them with Thread.startVirtualThread() and Thread.ofVirtual(), and ran 10,000 blocking tasks in about a second with newVirtualThreadPerTaskExecutor(), where a pool of platform threads would take far longer. We went through how virtual threads are mounted on and unmounted from carrier threads, the caveats of pinning, CPU-bound work and thread-locals, and when to use them. Next, read about record patternsClick to read tutorial: Java 21 Record Patterns with Examples.Continue Reading Java 18 to 21 Tutorials
Overview of New Features in Java 21Click to read tutorial: Overview of New Features in Java 21 Java 21 Record Patterns with ExamplesClick to read tutorial: Java 21 Record Patterns with Examples Java 21 Pattern Matching for switch with ExamplesClick to read tutorial: Java 21 Pattern Matching for switch with Examples Java 21 Sequenced Collections ExplainedClick to read tutorial: Java 21 Sequenced Collections Explained Java 21 Generational ZGC ExplainedClick to read tutorial: Java 21 Generational ZGC Explained Java 21 API Additions: StringBuilder.repeat, Math.clampClick to read tutorial: Java 21 API Additions: StringBuilder.repeat, Math.clamp Overview of New Features in Java 18Click to read tutorial: Overview of New Features in Java 18 Overview of New Features in Java 19Click to read tutorial: Overview of New Features in Java 19 Overview of New Features in Java 20Click to read tutorial: Overview of New Features in Java 20
Overview of New Features in Java 21Click to read tutorial: Overview of New Features in Java 21 Java 21 Record Patterns with ExamplesClick to read tutorial: Java 21 Record Patterns with Examples Java 21 Pattern Matching for switch with ExamplesClick to read tutorial: Java 21 Pattern Matching for switch with Examples Java 21 Sequenced Collections ExplainedClick to read tutorial: Java 21 Sequenced Collections Explained Java 21 Generational ZGC ExplainedClick to read tutorial: Java 21 Generational ZGC Explained Java 21 API Additions: StringBuilder.repeat, Math.clampClick to read tutorial: Java 21 API Additions: StringBuilder.repeat, Math.clamp Overview of New Features in Java 18Click to read tutorial: Overview of New Features in Java 18 Overview of New Features in Java 19Click to read tutorial: Overview of New Features in Java 19 Overview of New Features in Java 20Click to read tutorial: Overview of New Features in Java 20