Virtual Threads
Use Java 21 virtual threads for massive concurrency with blocking code, without the overhead of OS threads.
Overview
Key Points
- •Thread.ofVirtual().start(runnable) creates and starts a virtual thread
- •Thread.ofVirtual().unstarted(runnable) creates but does not start
- •Executors.newVirtualThreadPerTaskExecutor() creates an executor that uses one virtual thread per task
- •Virtual threads are created by the JVM and scheduled on carrier threads (platform threads from ForkJoinPool)
- •Blocking on I/O, sleep, or locks unmounts the virtual thread from its carrier without blocking the carrier
- •Platform threads are still the carriers; the JVM multiplexes virtual threads onto them
- •Virtual threads cannot be pooled; create a new one per task (they are cheap)
- •synchronized blocks can pin the virtual thread to its carrier; prefer ReentrantLock inside virtual threads
- •Structured concurrency (Java 21+) provides scope-based lifetime management for groups of virtual threads
Examples
Creating virtual threads vs platform threads
public class Main {
public static void main(String[] args) throws InterruptedException {
// Platform thread (traditional)
Thread platformThread = Thread.ofPlatform().start(() ->
System.out.println("Platform thread: " + Thread.currentThread().isVirtual()));
// Virtual thread
Thread virtualThread = Thread.ofVirtual().start(() ->
System.out.println("Virtual thread: " + Thread.currentThread().isVirtual()));
platformThread.join();
virtualThread.join();
}
}isVirtual() distinguishes virtual from platform threads. Virtual threads behave exactly like platform threads from the API perspective; the difference is in their lifecycle and scheduling.
Spawning 100,000 virtual threads concurrently
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class Main {
public static void main(String[] args) throws InterruptedException {
int taskCount = 100_000;
AtomicInteger completed = new AtomicInteger(0);
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < taskCount; i++) {
executor.submit(() -> {
// Simulate I/O wait
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
completed.incrementAndGet();
});
}
} // auto-shutdown and await termination
System.out.println("Completed: " + completed.get() + "/" + taskCount);
}
}Creating 100,000 platform threads would consume ~100GB of memory and fail. With virtual threads, this runs in seconds with minimal memory. The try-with-resources on ExecutorService shuts down and awaits completion.
Migrating a blocking HTTP service to virtual threads
import java.util.concurrent.*;
// Simulates a blocking service call (e.g., database, HTTP)
class OrderService {
String getOrder(int id) {
try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
return "Order#" + id;
}
}
public class Main {
public static void main(String[] args) throws Exception {
OrderService service = new OrderService();
int requests = 200;
long start = System.currentTimeMillis();
// Virtual thread per request: no thread pool sizing needed
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = new java.util.ArrayList<Future<String>>();
for (int i = 1; i <= requests; i++) {
final int id = i;
futures.add(executor.submit(() -> service.getOrder(id)));
}
for (Future<String> f : futures) {
f.get(); // just drain results
}
}
System.out.println(requests + " requests in " + (System.currentTimeMillis() - start) + "ms");
// Should be ~50ms, not 200 * 50ms, because blocking is free with virtual threads
}
}With platform threads and a fixed pool (e.g., 10 threads), 200 requests would take at minimum 200 * 50ms / 10 = 1000ms. With virtual threads, all 200 run concurrently and complete in ~50ms regardless of blocking time.
Common Mistakes
- Using virtual threads for CPU-bound work. Virtual threads do not make CPU work parallel; they only help with blocking I/O. For CPU parallelism, use ForkJoinPool or parallel streams.
- Pooling virtual threads. Virtual threads are cheap; a "virtual thread pool" is an anti-pattern. Use newVirtualThreadPerTaskExecutor() and let the JVM manage them.
- Using synchronized blocks that hold long-running I/O. synchronized pins the virtual thread to its carrier, blocking that carrier even during I/O. Use ReentrantLock for locks held across blocking operations.
- Assuming virtual threads are always faster. Virtual threads reduce thread overhead for I/O concurrency. Sequential code or purely CPU-bound code does not benefit.
Note
You can use virtual threads transparently with existing blocking I/O APIs (JDBC, HTTP clients, file reads). No code changes are needed except switching the executor. This is the key advantage: you do not need to rewrite blocking code as async callbacks to get high concurrency.
Exercises
Write Java code and click Run to execute it instantly. When your output looks correct, mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4