Learn how to use Java 21 virtual threads with Spring Boot 3 to build REST APIs that handle thousands of concurrent I/O-bound requests without thread-pool exhaustion.
TL;DR
-
Java 21 virtual threads provide lightweight concurrency for handling large numbers of I/O-bound requests.
-
Spring Boot 3.2+ can enable virtual threads with a single
spring.threads.virtual.enabled=trueproperty. -
Virtual threads work especially well for blocking I/O, such as database queries and calls to external services.
-
Structured concurrency can simplify parallel I/O when a request needs data from multiple sources.
-
Benchmark your workload before switching. Virtual threads help with concurrency and I/O, but they are not a replacement for optimizing CPU-heavy code.
This spring boot java 21 tutorial shows configuration, a working controller, and when virtual threads beat platform threads.
Prerequisites: Java basics and Spring Boot fundamentals. Complete Java Lesson 1 if you are new to Java.
Estimated time: 60–75 minutes. Difficulty: Intermediate.
What Are Virtual Threads?
Virtual threads (Project Loom, finalized in Java 21) are lightweight threads managed by the JVM rather than the operating system. A single platform thread can run thousands of virtual threads, making them ideal for I/O-bound workloads like HTTP clients, database queries, and file reads.
When to Use Virtual Threads in Spring Boot
| Use virtual threads | Stick with platform threads |
|---|---|
| REST APIs with blocking JDBC/JPA | CPU-heavy computation (image processing, ML inference) |
| Microservices calling external HTTP APIs | Code using synchronized blocks on hot paths (pinning) |
| High concurrency, moderate latency tolerance | Legacy libraries not yet virtual-thread friendly |
What You Need
- JDK 21 (LTS) — download from Adoptium
- Spring Boot 3.2+ (3.3 recommended)
- Maven 3.9+ or Gradle 8+
- Optional:
heyor Apache JMeter for load testing
Step 1: Create the Spring Boot 3 Project
At start.spring.io, select Java 21, Spring Boot 3.3.x, and add Spring Web. Name the project virtual-threads-demo.
<properties>
<java.version>21</java.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Step 2: Enable Virtual Threads
Add one line to application.properties:
spring.threads.virtual.enabled=true
Spring Boot 3.2+ configures Tomcat to use virtual threads for request handling automatically. No custom Executor bean is required for standard MVC controllers.
Step 3: Build a REST API with Blocking I/O
Create a controller that simulates I/O latency — the classic use case for virtual threads:
@RestController
@RequestMapping("/api/products")
public class ProductController {
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) throws InterruptedException {
// Simulate blocking database or HTTP call
Thread.sleep(200);
return new Product(id, "Widget " + id, 29.99);
}
@GetMapping
public List<Product> listProducts() throws InterruptedException {
Thread.sleep(100);
return List.of(
new Product(1L, "Widget 1", 19.99),
new Product(2L, "Widget 2", 29.99)
);
}
record Product(Long id, String name, double price) {}
}
Run the app:
mvn spring-boot:run
curl http://localhost:8080/api/products/1
Step 4: Structured Concurrency Example
Java 21’s structured concurrency lets you fan out parallel I/O safely:
@GetMapping("/aggregate/{id}")
public AggregatedProduct aggregate(@PathVariable Long id) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Subtask<Product> product = scope.fork(() -> fetchProduct(id));
Subtask<Reviews> reviews = scope.fork(() -> fetchReviews(id));
scope.join();
scope.throwIfFailed();
return new AggregatedProduct(product.get(), reviews.get());
}
}
This pattern works well in Spring services that call multiple downstream APIs — common in microservices like the InventoryMS Spring Boot API.
Step 5: Benchmark Platform vs Virtual Threads
Load-test with hey (install via Homebrew or GitHub releases):
# With virtual threads enabled (default after step 2)
hey -n 5000 -c 200 http://localhost:8080/api/products/1
# Disable virtual threads, restart, and compare:
# spring.threads.virtual.enabled=false
hey -n 5000 -c 200 http://localhost:8080/api/products/1
Frequently Asked Questions
Do I need to change my Spring Boot controller code for virtual threads?
No. Enable spring.threads.virtual.enabled=true and existing blocking MVC controllers run on virtual threads automatically. Avoid wrapping every call in CompletableFuture unless you have a specific reason.
Are virtual threads available in Java 17?
Virtual threads were preview/incubator in Java 19–20 and finalized in Java 21 LTS. Spring Boot 3.2+ requires Java 17 minimum but virtual thread support targets Java 21.
What is thread pinning and should I worry?
Pinning occurs when synchronized blocks or native code prevent a virtual thread from unmounting. Most Spring/JDBC drivers are pinning-safe in 2026, but profile CPU-bound synchronized code if throughput plateaus.
Do virtual threads work with Spring WebFlux?
WebFlux is already reactive/non-blocking. Virtual threads target the Servlet stack (Spring MVC + Tomcat). Use one model per service — don’t mix unnecessarily.
Can I use virtual threads with @Async?
Yes. Define a TaskExecutor bean using Executors.newVirtualThreadPerTaskExecutor() and reference it in @Async("virtualThreadExecutor").
How does this relate to the RAG chatbot tutorial?
AI chat endpoints are I/O-bound (embedding + LLM calls). Virtual threads let a RAG service handle many concurrent users — see the Spring Boot RAG tutorial.
Final Thoughts
Java 21 virtual threads make high-concurrency Spring Boot applications much easier to build without forcing developers to abandon the familiar servlet programming model. For applications that spend significant time waiting on databases, HTTP services, or other I/O operations, they can provide a practical way to handle more concurrent requests with less thread-management overhead.
The key is understanding where virtual threads help. They are designed for workloads that spend time waiting, not for making CPU-intensive operations faster. That is why benchmarking your actual application matters more than assuming virtual threads will automatically improve performance.
Start with the simple Spring Boot configuration, test it against a realistic workload, and measure the results. From there, you can decide whether virtual threads are the right fit for your REST API and downstream services.
Next Steps
- Build a RAG Chatbot with Spring Boot — AI + REST API
- Deploy Spring Boot to Kubernetes — production deployment
- Secure REST APIs with JWT in Spring Boot
- 15 Easy Free Java Tutorials
- Alkademy Java courses
Master modern Java? Join Alkademy for live Java 21 and Spring Boot courses with real-world projects.
[…] Spring Boot 3 + Java 21 Virtual Threads — scale concurrent chat requests […]