Spring Boot 3 Virtual Threads with Java 21: Scaling Web Apps Without Thread Pool Exhaustion
Without virtual threads, your Spring Boot application may exhaust its thread pool during high concurrency, causing request timeouts and HTTP 503 errors while CPU sits underutilized. Traditional thread-per-request models waste resources managing OS threads instead of processing actual work.
Prerequisites
- Java 21 JDK (OpenJDK 21.0.2 or later)
- Spring Boot 3.2.0+
- Maven 3.9+ or Gradle 8.4+
- Docker 24.0+ (for containerized testing)
- IntelliJ IDEA 2023.2+ or VS Code with Java extensions
Enabling Virtual Threads in Spring Boot 3.2
Spring Boot 3.2 introduces first-class support for Java 21's virtual threads through a simple configuration toggle. Add the Spring Boot parent POM and configure the virtual threads property:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
# Enable Java 21 virtual threads
spring.threads.virtual.enabled=true
This setting replaces the default Tomcat thread pool with a virtual thread executor, allowing millions of concurrent requests without increasing OS thread count.
Implementing Virtual Thread-Powered REST Endpoints
Create non-blocking HTTP handlers that leverage virtual threads for CPU-bound operations. This controller executes parallel image processing using virtual threads:
package com.example.vthreads;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@RestController
@RequestMapping("/images")
public class ImageController {
private final ExecutorService virtualExecutor =
Executors.newVirtualThreadPerTaskExecutor();
@PostMapping("/process")
public String processImage(@RequestBody byte[] imageData) {
virtualExecutor.execute(() ->
ImageProcessor.applyFilters(imageData)
);
return "Processing started in virtual thread";
}
}
Test with curl -X POST http://localhost:8080/images/process -d @large_image.jpg and monitor thread count in logs. Virtual threads will appear with ForkJoinPool identifiers instead of tomcat threads.
Common Mistakes
Mistake 1: Using Spring Boot 3.1 with Java 21
<!-- WRONG -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.5</version>
</parent>
<!-- FIX -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
Spring Boot 3.1 doesnโt support the virtual threads property. The 3.2 parent POM adds Java 21 compatibility.
Mistake 2: Missing Virtual Threads Configuration
# WRONG: Property missing
# spring.threads.virtual.enabled=true
# FIX
spring.threads.virtual.enabled=true
Without this flag, Spring Boot uses platform threads even on Java 21. The property activates the virtual thread executor.
Mistake 3: Blocking in Virtual Threads
// WRONG
virtualExecutor.execute(() -> {
Thread.sleep(1000); // Blocks carrier thread
});
// FIX
virtualExecutor.execute(() -> {
try {
Thread.sleep(Duration.ofSeconds(1)); // Yields carrier thread
} catch (InterruptedException e) { /* ... */ }
});
Use Duration-based sleep instead of millisecond sleep to enable proper thread yielding in virtual threads.
Summary
- Activate virtual threads with
spring.threads.virtual.enabled=truein Spring Boot 3.2+ applications - Replace
@Asyncand thread pools withExecutors.newVirtualThreadPerTaskExecutor()for CPU-bound tasks - Monitor virtual thread behavior using
/actuator/metrics/jvm.threads.virtualendpoint
The author publishes Spring Boot starter templates at https://gumroad.com
Top comments (0)