Virtual threads are one of the most exciting additions to modern Java. They make it much easier to write highly concurrent applications without the complexity of managing large thread pools, callbacks, or reactive pipelines.
If you build applications with Spring Boot, virtual threads can help your app handle many more concurrent tasks with simpler code.
In this post, we’ll cover:
- What virtual threads are
- Why they matter
- When to use them
- How to enable them in Spring Boot
- A few important best practices and warnings
What are virtual threads?
Traditional Java threads are often called platform threads. They are mapped closely to operating system threads. That means they are relatively expensive in terms of memory and scheduling.
Virtual threads are lightweight threads managed by the JVM, not directly by the operating system. They are designed to make blocking code cheap again.
In simple terms:
- Platform threads = heavier, fewer, more expensive
- Virtual threads = lightweight, many more, cheaper to create
This means you can write code in the usual imperative style, but still support high concurrency.
Why virtual threads matter
For years, Java developers had two main choices for handling concurrency:
- Use thread pools and blocking code
- Use asynchronous/reactive programming
Virtual threads give you a third option:
- Keep the simple blocking style
- Avoid the complexity of reactive code
- Scale better under lots of concurrent I/O operations
This is especially useful for applications that spend a lot of time waiting on:
- Database calls
- HTTP requests
- File I/O
- Remote service calls
If your application is mostly I/O-bound, virtual threads can be a great fit.
Virtual threads vs platform threads
Here’s a simple comparison:
| Feature | Platform Threads | Virtual Threads |
|---|---|---|
| Cost to create | Higher | Very low |
| Memory usage | Higher | Lower |
| Number you can run | Limited | Much larger |
| Good for blocking code | Yes, but expensive | Yes, and efficient |
| Managed by OS | Yes | Mostly by JVM |
The biggest win is that virtual threads let you run many blocking tasks concurrently without exhausting thread resources as quickly.
When should you use virtual threads?
Virtual threads are a strong choice when your app does lots of blocking I/O and you want simple code.
Good use cases:
- REST APIs with many simultaneous requests
- Service-to-service communication
- Database-heavy applications
- Background jobs that wait on I/O
- File processing or batch operations
Less ideal use cases:
- CPU-intensive tasks
- Work that depends heavily on thread-local assumptions
- Libraries that block while holding locks in a problematic way
Virtual threads do not magically make CPU-bound code faster. They help most when threads spend time waiting.
Project Loom in Java
Virtual threads are part of Project Loom, a long-running effort to modernize concurrency in Java.
Loom also introduced other improvements around structured concurrency and scoped values, but the headline feature most developers use today is virtual threads.
Virtual threads became a standard Java feature in recent releases, so you no longer need special preview settings in modern Java versions.
How Spring Boot supports virtual threads
Spring Boot has first-class support for virtual threads.
If you are using Spring Boot 3.2+, you can usually enable them with a simple configuration property:
spring.threads.virtual.enabled=true
That tells Spring Boot to use virtual threads in places where it manages request handling and task execution.
This is one of the nicest parts: in many cases, you can get the benefits of virtual threads with very little code change.
Basic setup in Spring Boot
1. Use a recent Java version
Virtual threads require a modern JDK, for example Java 25.
2. Use a recent Spring Boot version
Make sure your Spring Boot version supports virtual threads well. Spring Boot 3.2 or later is recommended.
3. Enable virtual threads
Add this to your application configuration:
spring.threads.virtual.enabled=true
Or in YAML:
spring:
threads:
virtual:
enabled: true
That’s often enough for web applications.
What happens when you enable them?
When virtual threads are enabled, Spring can use them for tasks such as:
- Handling incoming HTTP requests
- Running
@Asyncmethods - Executing some scheduled or background tasks
The exact behavior depends on the Spring component and configuration, but the overall idea is that work can run on virtual threads instead of a small pool of platform threads.
Example: a Spring Boot REST controller
Here is a simple example of how your code can stay clean and blocking while still benefiting from virtual threads.
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
private final DemoService demoService;
public DemoController(DemoService demoService) {
this.demoService = demoService;
}
@GetMapping("/hello")
public String hello() {
return demoService.fetchMessage();
}
}
package com.example.demo;
import org.springframework.stereotype.Service;
@Service
public class DemoService {
public String fetchMessage() {
// Simulate a blocking operation
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Hello from a virtual thread!";
}
}
With virtual threads enabled, each request can be handled with a lightweight thread, even though the service method blocks.
Example: using virtual threads for background tasks
You can also create virtual threads manually when needed.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class VirtualThreadExample {
public static void main(String[] args) {
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> System.out.println("Task 1 on virtual thread"));
executor.submit(() -> System.out.println("Task 2 on another virtual thread"));
}
}
}
This is useful when you want a simple executor that creates a new virtual thread for each task.
Using @Async with virtual threads
Spring’s @Async support can also benefit from virtual threads.
For example:
package com.example.demo;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class EmailService {
@Async
public CompletableFuture<String> sendEmail() {
// Simulate work
return CompletableFuture.completedFuture("Email sent");
}
}
If Spring is configured to use virtual threads for task execution, these async methods can run on virtual threads, making blocking work much cheaper.
Virtual threads do not replace everything
It’s important not to oversell virtual threads. They solve one problem very well: cheap blocking concurrency.
They do not replace:
- Proper database indexing
- Efficient network design
- Good application architecture
- Caching
- Load balancing
- Performance tuning
Virtual threads are a concurrency tool, not a performance silver bullet.
Best practices when using virtual threads
1. Keep code simple
One of the main advantages of virtual threads is that you can keep using normal blocking code. Don’t add unnecessary complexity.
2. Avoid long synchronized blocks
Virtual threads can behave poorly if they spend too much time blocked inside synchronized sections. Prefer shorter critical sections and consider alternatives where appropriate.
3. Watch out for thread-local usage
Some older code relies heavily on ThreadLocal. Virtual threads support it, but large-scale usage can still create complexity and memory overhead.
4. Test your dependencies
Most modern libraries work well, but some older libraries may assume platform-thread behavior or may not play nicely with high concurrency.
5. Don’t use virtual threads for CPU-heavy work expecting miracles
If your bottleneck is CPU, virtual threads will not fix it. For CPU-bound tasks, focus on algorithms, parallelism, and profiling.
Common questions
Are virtual threads faster?
Not always in a direct “single task runs faster” sense. Their big advantage is that they allow more concurrency with less overhead, especially for blocking I/O.
Do I need reactive programming anymore?
Not necessarily. Virtual threads reduce the need for reactive programming in many applications, especially if you prefer imperative code.
Can I use virtual threads with Spring MVC?
Yes. Spring MVC is a great fit because it already uses a blocking request model, which maps naturally to virtual threads.
Can I use them with Spring WebFlux?
You can, but WebFlux is built around a reactive model. Virtual threads are often more valuable in traditional blocking stacks like Spring MVC.
When virtual threads are a great fit in Spring Boot
Virtual threads are especially attractive if:
- You already have a Spring MVC application
- You use JDBC and blocking database access
- Your codebase is imperative and you want to keep it that way
- You want to handle more concurrent requests with less thread-pool tuning
In many cases, virtual threads let you modernize your app without rewriting it.
A practical migration strategy
If you want to adopt virtual threads in an existing Spring Boot app, here’s a simple approach:
- Upgrade to a compatible Java and Spring Boot version
- Enable virtual threads in configuration
- Test your main request paths
- Watch application metrics under load
- Check library compatibility
- Tune only where needed
You usually do not need to rewrite your whole application.
Final thoughts
Virtual threads are a major step forward for Java concurrency. They make it possible to write straightforward, blocking-style code while still supporting high throughput and scalability.
For Spring Boot developers, this is especially valuable because it means:
- Less concurrency boilerplate
- Easier-to-read code
- Better scaling for I/O-heavy workloads
- A smoother path than fully reactive programming for many apps
If your application spends a lot of time waiting on I/O, virtual threads are absolutely worth trying.
Summary
Use virtual threads in Spring Boot when you want:
- Simple blocking code
- High concurrency
- Less thread-pool management
- Better scalability for I/O-bound workloads
Enable them with:
spring.threads.virtual.enabled=true
Then test, measure, and enjoy a much simpler concurrency model in Java.
