DEV Community

Nitish
Nitish

Posted on

Spring Boot 3 Virtual Threads with Java 21: Scaling Web Apps Without Thread Pool Exhaustion

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>
Enter fullscreen mode Exit fullscreen mode
# Enable Java 21 virtual threads
spring.threads.virtual.enabled=true
Enter fullscreen mode Exit fullscreen mode

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";
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode
<!-- FIX -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# FIX
spring.threads.virtual.enabled=true
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode
// FIX
virtualExecutor.execute(() -> {
    try {
        Thread.sleep(Duration.ofSeconds(1)); // Yields carrier thread
    } catch (InterruptedException e) { /* ... */ }
});
Enter fullscreen mode Exit fullscreen mode

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=true in Spring Boot 3.2+ applications
  • Replace @Async and thread pools with Executors.newVirtualThreadPerTaskExecutor() for CPU-bound tasks
  • Monitor virtual thread behavior using /actuator/metrics/jvm.threads.virtual endpoint

The author publishes Spring Boot starter templates at https://gumroad.com

Top comments (0)