Virtual Threads — Practical Patterns
Use Java 21 virtual threads for high-concurrency I/O: per-request threads, structured scopes, and limits.
import java.time.*;
import java.util.concurrent.*;
import java.util.stream.*;
import java.util.List;
public class VirtualThreadPatterns {
// 1. Simple virtual thread
static void basic() throws InterruptedException {
Thread vt = Thread.ofVirtual().name("worker-1").start(() -> {
System.out.println(Thread.currentThread());
});
vt.join();
}
// 2. Virtual thread executor — one thread per task
static void executorPerTask() throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = IntStream.range(0, 10_000)
.mapToObj(i -> executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // I/O sim
return "result-" + i;
}))
.toList();
long completed = futures.stream()
.map(f -> { try { return f.get(); } catch (Exception e) { return null; } })
.filter(r -> r != null)
.count();
System.out.println("Completed: " + completed);
}
}
// 3. Bounded virtual threads (limit concurrency)
static void bounded() throws Exception {
Semaphore limiter = new Semaphore(100); // max 100 concurrent
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 1000).forEach(i ->
executor.submit(() -> {
try {
limiter.acquire();
try {
Thread.sleep(50); // simulate I/O
return "done-" + i;
} finally {
limiter.release();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return "interrupted-" + i;
}
}));
}
System.out.println("All bounded tasks done");
}
// 4. HTTP server pattern (one VT per request)
static void serverPattern() throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// Simulate incoming requests
for (int i = 0; i < 100; i++) {
final int reqId = i;
executor.submit(() -> handleRequest(reqId));
}
}
}
static String handleRequest(int id) {
try {
// Each request gets its own virtual thread
String dbResult = queryDb(id);
String cacheResult = queryCache(id);
return dbResult + " | " + cacheResult;
} catch (Exception e) {
return "error: " + e.getMessage();
}
}
static String queryDb(int id) throws InterruptedException {
Thread.sleep(50); // simulate DB
return "db-" + id;
}
static String queryCache(int id) throws InterruptedException {
Thread.sleep(10); // simulate cache
return "cache-" + id;
}
// 5. ThreadLocal alternative: ScopedValue (preview)
// static final ScopedValue<String> USER = ScopedValue.newInstance();
// ScopedValue.where(USER, "alice").run(() -> {
// System.out.println(USER.get()); // alice
// });
public static void main(String[] args) throws Exception {
long start = System.nanoTime();
basic();
executorPerTask();
bounded();
serverPattern();
System.out.printf("Total: %d ms%n", (System.nanoTime() - start) / 1_000_000);
}
}Use Cases
- High-concurrency I/O-bound servers
- Replacing thread pools for blocking workloads
- Scalable microservice request handling
Tags
Related Snippets
Similar patterns you can reuse in the same workflow.
Virtual Threads — Lightweight Concurrency
Use Java 21 virtual threads for massive concurrency without thread pool tuning or reactive frameworks.
Best for: High-concurrency web servers handling thousands of requests
CompletableFuture — Async Programming
Chain async operations with CompletableFuture: thenApply, thenCompose, allOf, and exception handling.
Best for: Parallel API calls to multiple services
ExecutorService — Thread Pool Management
Create and manage thread pools with ExecutorService: fixed, cached, scheduled, and custom pools.
Best for: Managing concurrent task execution
Concurrent Collections — Thread-Safe Maps
Use ConcurrentHashMap, CopyOnWriteArrayList, and BlockingQueue for thread-safe data structures.
Best for: Thread-safe caching in multi-threaded applications