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.

ConcurrencyVirtual ThreadsJava 21Modern JavaProject Loom

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.

Virtual threads are not magic faster CPU 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.

java
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 often not CPU.
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 threads

When 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

java
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:

java
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.

With virtual threads, the common model is:
one task = one virtual thread

Why This Helps Blocking I/O Code

Many backend requests spend most of their time waiting:

text
Request starts
Call database
Wait
Call payment service
Wait
Call inventory service
Wait
Return response

With platform threads, each waiting request ties up an expensive OS thread. With virtual threads, waiting is much cheaper.

Virtual threads let you keep simple blocking-style code without needing reactive-style callbacks for every I/O-heavy workload.

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.

java
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.

Virtual threads make threads less scarce.
They do not make databases, sockets, memory, or downstream services unlimited.

Platform Threads vs Virtual Threads

FeaturePlatform ThreadVirtual Thread
Backed byOS threadJDK-managed
CostExpensiveLightweight
Typical usageThread poolOne virtual thread per task
Best workloadGeneral purposeHigh-concurrency blocking I/O
More CPU power?NoNo

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.

Virtual threads reduce the cost of threads.
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.

Virtual threads make concurrency cheaper.
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.

properties
spring.threads.virtual.enabled=true

The benefit is that your controller and service code can often stay written in a normal blocking style.

java
@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.

Virtual threads let Spring Boot keep the simple thread-per-request mental model, but make each request thread much cheaper when it spends time waiting on I/O.

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.

Important warning: virtual threads make threads cheaper, not downstream systems unlimited. You still need database connection pool limits, HTTP client limits, rate limits, timeouts, retries, and backpressure.

A good interview answer is:

In Spring Boot, virtual threads can be enabled so request handling can use lightweight virtual threads instead of relying only on a limited pool of platform threads. This is useful for blocking I/O-heavy REST APIs because it preserves simple blocking code while improving concurrency. But it does not remove the need to protect databases, downstream services, and other limited resources.

Interview-Friendly Explanation

Virtual threads are lightweight Java threads finalized in Java 21. They are useful for high-concurrency applications where many tasks block on I/O. They let developers keep a simple thread-per-task style without tying every task to an expensive OS thread. They help with scalability for blocking workloads, but they do not speed up CPU-bound work or remove the need for resource limits.

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.

Final Takeaway

Virtual threads make blocking-style concurrency much cheaper and easier to scale. They are best for high-concurrency I/O-heavy workloads, not for making CPU-heavy code magically faster. Use them with good resource limits, timeouts, and structured task management.