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)