How to use virtual threads
Short answer
Virtual threads (Java 21) are very lightweight: you can create thousands of them. The easiest way is an executor that runs every task in a new virtual thread:
void main() {
var done = new AtomicInteger();
long start = System.nanoTime();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(500));
return done.incrementAndGet();
});
}
}
long ms = (System.nanoTime() - start) / 1_000_000;
IO.println(done.get() + " tasks, less than 3 s: " + (ms < 3000));
}Output:
10000 tasks, less than 3 s: true10,000 tasks that each wait half a second finish in about half a second. With a regular pool of 10 threads it would take more than eight minutes.
A single thread
Thread.ofVirtual().start() starts one virtual thread, and join() waits for it to finish:
void main() throws InterruptedException {
Thread thread = Thread.ofVirtual().start(() -> IO.println("Hello from a virtual thread"));
thread.join();
IO.println(thread.isVirtual());
}Output:
Hello from a virtual thread
trueWhen they fit
Virtual threads are good for tasks that mostly wait: network calls, database queries, files. While a thread waits, the JVM gives the CPU to others. For heavy computations they do not help — they do not add CPU cores.
Common mistake
Putting virtual threads into a fixed-size pool. There is no need to save them: create a new thread for every task. If you need to limit load, for example the number of concurrent database queries, use a Semaphore.
Functions used in this recipe
- Executors.newVirtualThreadPerTaskExecutor() — Creates a service that runs every task in its own virtual thread.
- Thread.ofVirtual() — Creates a virtual thread — a lightweight thread you can start by the million.
- Thread.sleep() — Pauses the current thread for a given time.