Modern Java + Concurrency
Virtual Threads in Java
Virtual threads are lightweight Java threads finalized in Java 21. They make thread-per-task style programming practical for many high-concurrency blocking I/O workloads.
The Short Answer
Virtual threads are lightweight Java threads introduced as a final feature in Java 21.
They let you write simple blocking-style code while supporting many more concurrent tasks than traditional platform threads.
They are cheap threads that are especially useful when your code spends a lot of time waiting on I/O.
The Problem With Traditional Platform Threads
Before virtual threads, Java threads were usually platform threads. A platform thread is backed by an operating system thread.
Platform threads are powerful, but they are relatively expensive. Because of that, backend applications usually use thread pools.
ExecutorService executor =
Executors.newFixedThreadPool(100);That creates a limit. If all 100 threads are blocked waiting for database calls, HTTP calls, or file I/O, new requests must wait even if the CPU is mostly idle.
The bottleneck is that too many expensive platform threads are stuck waiting.
Virtual Thread Mental Model
Platform thread
Heavyweight thread backed by an operating system thread.
Virtual thread
Lightweight Java thread managed by the JDK and mounted on carrier threads when running.
Many virtual threads
|
v
+-------------------+
| Java runtime |
+-------------------+
|
v
Smaller number of platform/carrier threadsWhen a virtual thread blocks on many common blocking operations, the JDK can unmount it from the carrier thread. The carrier thread can then run something else.
Simple Virtual Thread Example
public class VirtualThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread thread =
Thread.startVirtualThread(() -> {
System.out.println(
"Running in " + Thread.currentThread()
);
});
thread.join();
}
}This creates and starts one virtual thread.
Virtual Thread Per Task Executor
In server-style code, you will often see this pattern:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualThreadExecutorExample {
public static void main(String[] args) {
try (ExecutorService executor =
Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int requestId = i;
executor.submit(() -> {
handleRequest(requestId);
});
}
}
}
static void handleRequest(int requestId) {
// Blocking-looking code is okay here
System.out.println("Handling request " + requestId);
}
}Instead of creating a small fixed pool and reusing threads, this style creates a new virtual thread for each task.
one task = one virtual thread
Why This Helps Blocking I/O Code
Many backend requests spend most of their time waiting:
Request starts
↓
Call database
↓
Wait
↓
Call payment service
↓
Wait
↓
Call inventory service
↓
Wait
↓
Return responseWith platform threads, each waiting request ties up an expensive OS thread. With virtual threads, waiting is much cheaper.
What Virtual Threads Are Good For
Blocking I/O
HTTP calls, database calls, file I/O, and service-to-service calls.
High concurrency
Many simultaneous requests where each request waits often.
Simple code style
Keep straightforward imperative code instead of callback-heavy async code.
Thread-per-request
Make one thread per request practical again for many server workloads.
What Virtual Threads Are Not Good For
CPU-bound work
If all tasks are busy using CPU, virtual threads do not create more CPU cores.
Unbounded external calls
You still need rate limits, connection pools, and backpressure.
Magic performance fixes
They reduce thread scarcity, but they do not fix slow queries, bad algorithms, or overloaded dependencies.
Ignoring resource limits
You can create many virtual threads, but databases and downstream services still have limits.
Important: Virtual Threads Do Not Remove The Need For Limits
This is one of the biggest practical mistakes.
Because virtual threads are cheap, it is tempting to start a huge number of tasks and assume everything will be fine.
for (String url : urls) {
executor.submit(() -> callRemoteService(url));
}But the remote service, database, connection pool, or API rate limit may not handle that much traffic.
They do not make databases, sockets, memory, or downstream services unlimited.
Platform Threads vs Virtual Threads
| Feature | Platform Thread | Virtual Thread |
|---|---|---|
| Backed by | OS thread | JDK-managed |
| Cost | Expensive | Lightweight |
| Typical usage | Thread pool | One virtual thread per task |
| Best workload | General purpose | High-concurrency blocking I/O |
| More CPU power? | No | No |
Virtual Threads And ThreadLocal
Virtual threads are still Java Thread objects, so they can use ThreadLocal.
However, there is an important practical warning: if you create a huge number of virtual threads and each one stores large ThreadLocal values, memory usage can still become a problem.
They do not make per-thread memory free.
How This Relates To Structured Concurrency
Virtual threads and structured concurrency solve different but related problems.
Virtual threads
Make it cheap to run many concurrent blocking-style tasks.
Structured concurrency
Organizes related tasks so they are cancelled, joined, and handled as a group.
Structured concurrency makes concurrency easier to reason about.
Can Spring Boot REST APIs Use Virtual Threads?
Yes. In a typical Spring Boot REST API using Spring MVC, each incoming request is handled by a thread.
Traditionally, that request thread is a platform thread from the servlet container's thread pool. With virtual threads enabled, Spring Boot can handle request processing using virtual threads instead.
spring.threads.virtual.enabled=trueThe benefit is that your controller and service code can often stay written in a normal blocking style.
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable long id) {
Order order = orderService.findOrder(id);
Customer customer = customerClient.getCustomer(order.customerId());
return OrderDto.from(order, customer);
}This code may block while waiting for the database or another HTTP service. With platform threads, each blocked request holds onto an expensive operating system thread. With virtual threads, that waiting is much cheaper.
This does not mean every Spring Boot app automatically becomes faster. Virtual threads help most when the app is handling many concurrent requests that spend time waiting on blocking I/O, such as database calls, HTTP calls, or filesystem calls.
They help much less for CPU-heavy work, because virtual threads do not create more CPU cores.
A good interview answer is:
Interview-Friendly Explanation
Common Interview Follow-Ups
Are virtual threads faster than platform threads?
Not in the sense of making CPU work faster. They are cheaper to create and block, which helps high-concurrency I/O-bound applications.
Do virtual threads replace thread pools?
For many blocking I/O tasks, the model shifts from a small fixed pool to one virtual thread per task. But you may still need limits around external resources.
Are virtual threads useful for CPU-bound work?
Usually not much. CPU-bound work is limited by CPU cores. Virtual threads help most when tasks spend time waiting.
Can existing blocking code use virtual threads?
Often yes. One of the big advantages is that blocking-style code can often run on virtual threads with much less rewriting than reactive code.
What is the biggest mistake with virtual threads?
Assuming that cheap threads make all resources unlimited. Databases, APIs, sockets, memory, and downstream services still need limits and protection.