Virtual threads
Virtual threads (final in Java 21, from Project Loom) are lightweight threads managed by the JVM rather than the operating system. When a virtual thread blocks on I/O, the JVM parks it and frees the underlying carrier thread for other work.
Why it matters: the simple thread-per-request style (blocking JDBC, RestClient) now scales to hundreds of thousands of concurrent requests without reactive code.
- Create one with
Thread.ofVirtual().start(...)orExecutors.newVirtualThreadPerTaskExecutor(). - In Spring Boot 3.2+, set
spring.threads.virtual.enabled=true. - Don't pool virtual threads; create one per task.
- They help I/O-bound work. CPU-bound work gains nothing.
Example
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> results = IntStream.range(0, 10_000)
.mapToObj(i -> executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1)); // blocking is fine here
return "task " + i;
}))
.toList();
System.out.println(results.getLast().get()); // about 1 second in total, not 10,000
}Common mistake
Putting virtual threads in a fixed-size pool, or expecting CPU-heavy work to run faster. They increase how many tasks can wait at once, not raw computing speed.
Under the hood
Virtual threads are cheap because their stacks live on the heap as small, growable chunks. Watch for: blocking inside synchronized pinned the carrier before Java 24 (JEP 491 fixed it); ThreadLocal with large objects multiplies memory across millions of threads, so prefer scoped values (final in Java 25). Downstream limits still apply: 10,000 virtual threads will happily exhaust a 10-connection database pool, so guard scarce resources with a Semaphore.
Check yourself
Should you pool virtual threads?
How this connects
Know these first
Where this leads
You've reached the end of this thread. Try a learning path for what's next.
Part of Crack the Java interview, Upgrade from Java 8 to Java 25, Microservices and production.
Was this lesson helpful?
Finished reading? Mark it complete to track your progress.