Millions of cheap threads — how Project Loom changes Java server-side concurrency.
Published February 20, 2025
A traditional Java thread maps 1:1 to an OS thread. OS threads are expensive:
This is why Node.js and reactive frameworks (WebFlux) were invented — to handle more concurrent requests without more threads.
Virtual threads are lightweight, JVM-managed threads:
// 100,000 virtual threads — this works
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 100_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // blocks virtual thread, not OS thread
System.out.println("Done: " + i);
})
);
}
// Completes in ~1 second — all 100K sleep concurrently
spring:
threads:
virtual:
enabled: true
With this property, Spring Boot replaces its Tomcat thread pool with virtual threads. Each incoming HTTP request runs on its own virtual thread.
✅ I/O-bound workloads: REST calls, DB queries, file reads — virtual threads shine here
❌ CPU-bound workloads: image processing, cryptography — virtual threads don't help because the carrier thread is occupied the entire time
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<User> user = scope.fork(() -> fetchUser(userId));
Future<Order> order = scope.fork(() -> fetchOrder(orderId));
scope.join(); // wait for both
scope.throwIfFailed();
return new Response(user.resultNow(), order.resultNow());
}
Structured concurrency ensures child tasks are cleaned up when the parent scope exits — no more fire-and-forget threads.
"Virtual threads eliminate the need for reactive programming for I/O-bound workloads. They let you write blocking-style synchronous code that performs like async code — without the complexity of CompletableFuture chains or WebFlux."
Key caveat: avoid synchronised blocks with virtual threads. synchronized pins the virtual thread to its carrier thread, negating the benefit. Use ReentrantLock instead.