How to Use Java 21’s Virtual Threads in Real-World Web Applications
With the release of Java 21, virtual threads — part of Project Loom — are now stable and production-ready. This revolutionary feature enables developers to write high-throughput, scalable concurrent applications with a simplified programming model. For Spring Boot developers, especially those using version 3.2 or newer, virtual threads unlock a new era of performance for I/O-bound web applications.
In this article, you’ll learn:
- What virtual threads are and how they differ from traditional threads.
- How to integrate them into Spring Boot 3.2+ applications.
- Real-world examples.
- Best practices and caveats.
- Helpful references and resources.
☁️ What Are Virtual Threads?
Virtual threads are lightweight threads that are managed by the JVM, not the operating system. They’re designed to dramatically reduce the cost of concurrent programming.
🔍 Key Benefits:
- Near-zero overhead in thread creation.
- Enables writing blocking code with the scalability of asynchronous models.
- Excellent for I/O-bound applications like web servers.
🧵 Traditional threads: ~2MB of stack memory
🪶 Virtual threads: ~few KB and managed by JVM, not OS kernel
📖 Java Virtual Threads Documentation
⚙️ Enabling Virtual Threads in Spring Boot 3.2+
Spring Boot 3.2+ introduces first-class support for virtual threads via configuration and the updated TaskExecutor API.
✅ Prerequisites
- Java 21+
- Spring Boot 3.2 or higher
- Spring Web or WebFlux
🛠️ Example: Virtual Threads in Spring MVC Controller
@RestController
@RequestMapping("/api")
public class VirtualThreadController {
@GetMapping("/process")
public String processRequest() {
try {
Thread.sleep(2000); // Simulate I/O-bound operation
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return "Processed by thread: " + Thread.currentThread();
}
}
With virtual threads, this blocking Thread.sleep() won’t harm scalability.
🧰 Step-by-Step Configuration
1. Use Virtual Thread Executor
@Configuration
public class VirtualThreadConfig {
@Bean
public Executor taskExecutor() {
return Executors.newVirtualThreadPerTaskExecutor();
}
}
This sets up an Executor that creates a new virtual thread for every task.
ℹ️ This configuration is used by Spring’s
@Async,RestTemplate, and other task-based components.
2. Enable Asynchronous Methods
@EnableAsync
@SpringBootApplication
public class VirtualThreadApp {
public static void main(String[] args) {
SpringApplication.run(VirtualThreadApp.class, args);
}
}
Now, any @Async method will use virtual threads:
@Async
public CompletableFuture<String> heavyComputation() {
Thread.sleep(3000);
return CompletableFuture.completedFuture("Done in " + Thread.currentThread());
}
🌐 Real-World Scenario: Handling Thousands of Concurrent HTTP Requests
Imagine a REST API that queries a remote service for stock data. Normally, this would require reactive code to achieve scalability. But with virtual threads, you can stay imperative:
@GetMapping("/stocks")
public ResponseEntity<String> getStockInfo() throws IOException {
URL url = new URL("https://api.example.com/stock/ABC");
try (BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()))) {
String response = in.readLine();
return ResponseEntity.ok(response);
}
}
This blocking I/O operation is efficient and scalable with virtual threads — no need to rewrite using WebClient or reactive paradigms.
🧪 Benchmark: Loom vs. Traditional Threads
A simple comparison on a Spring Boot server with:
- 10,000 concurrent requests
- Each request waits for 2s (simulating DB call)
| Thread Model | Average Latency | CPU Usage | Memory Footprint |
|---|---|---|---|
| Platform Threads | High | High | ~20GB (OOM likely) |
| Virtual Threads | Low | Moderate | ~2GB |
📚 Official Performance Benchmarks (JEP 444)
✅ Best Practices
- Avoid CPU-bound work in virtual threads: They’re ideal for I/O-heavy workloads.
- Use structured concurrency (Java 21 preview) to manage thread lifecycles.
- Profile your application to identify blocking points (e.g., JDBC calls).
- Use
Thread.ofVirtual().start()for ad-hoc concurrency outside of Spring:
Thread.startVirtualThread(() -> {
// some blocking task
});
⚠️ Gotchas
- Not all libraries are virtual-thread friendly. Watch out for native synchronization primitives (
synchronized,wait()). - Connection pool exhaustion: You still need to tune database pools (or use R2DBC).
- Monitoring: Many observability tools don’t yet fully support virtual threads.
📖 See Spring Docs on Virtual Threads
🧩 Combining Virtual Threads with Structured Concurrency
Structured concurrency (preview feature in Java 21) allows managing task hierarchies cleanly:
try (var scope = StructuredTaskScope.ShutdownOnFailure.open()) {
Future<String> task1 = scope.fork(() -> fetchData());
Future<String> task2 = scope.fork(() -> computeSomething());
scope.join();
scope.throwIfFailed();
return task1.result() + task2.result();
}
📘 JEP 453 – Structured Concurrency (Preview)
🔚 Conclusion
Virtual threads in Java 21 bring back the simplicity of synchronous code with the scalability of asynchronous models. For Spring Boot 3.2+ developers, this means writing readable, efficient web apps capable of handling massive concurrency without the complexity of reactive frameworks.
Now you can:
- Embrace blocking I/O with confidence.
- Simplify your codebase.
- Improve scalability without rewriting everything in a reactive style.
🔗 Further Reading
- Java 21: Virtual Threads Deep Dive (Baeldung)
- Project Loom on OpenJDK
- Spring Blog: Virtual Threads Support in Spring Framework 6.1
- Thread Dump Analysis for Virtual Threads (JetBrains)

