For years we wrapped blocking I/O in thread pools and reactive frameworks to avoid exhausting OS threads. Virtual threads flip that: they are managed by the JVM, cost almost nothing, and let you write straightforward blocking code.
Spawn freely
A virtual thread parks instead of blocking an OS thread when it hits I/O. That means one thread per task is suddenly a reasonable design.
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (var request : requests) {
executor.submit(() -> {
var response = httpClient.send(request); // blocking is fine
store(response);
});
}
}Enable them in Spring Boot
One property switches Tomcat and the task executor to virtual threads.
spring.threads.virtual.enabled=trueTakeaways
- Use virtual threads for I/O-bound work, not CPU-bound number crunching.
- Avoid pooling them — create one per task; they are disposable.
- Watch for
synchronizedblocks around I/O, which can pin the carrier thread.
Reach for ReentrantLock instead of synchronized when guarding blocking
sections, so virtual threads can unmount cleanly.