Explore Spring Boot virtual threads and learn how to enable them effortlessly with step-by-step configuration tips and practical examples. Master concurrency in Java applications for optimal performance.
1. Introduction
In our earlier blog on Java virtual threads, we explored how Java’s virtual threads revolutionize concurrent programming. Now, let’s delve into Spring Boot Virtual Threads, an exciting development in the Spring ecosystem. We’ll cover everything from basics to configuration and practical examples ensuring a smooth understanding for Spring developers.
2. What are Spring Boot Virtual Threads?
Spring Boot Virtual Threads leverage Java’s virtual threads to enhance concurrency in Spring Boot applications. They offer lightweight, scalable, and efficient multitasking, allowing apps to handle more simultaneous tasks without traditional thread overhead.
3. Benefits of Spring Boot Virtual Threads
Spring Boot virtual threads offer several benefits:
- Minimal Configuration: Spring Boot virtual threads can be enabled with minimal configuration settings. This means developers can leverage the benefits of virtual threads without extensive changes to their existing codebase.
- No Additional Logic: Virtual threads in Spring Boot do not require developers to write additional logic or special programming constructs. Once enabled, virtual threads handle concurrency and resource management internally, reducing the need for manual intervention.
- Compatibility with Existing Applications: Existing Spring Boot applications running on Spring Framework 6 or Spring Boot 3 can seamlessly adopt virtual threads without major code refactoring. This compatibility ensures that applications can take advantage of performance enhancements without significant architectural changes.
- Performance Enhancements: Spring Boot virtual threads offer performance enhancements by improving concurrency, reducing resource consumption, and optimizing task execution. This leads to faster response times, better scalability, and overall improved application performance.
4. Prerequisite for Enabling Virtual Threads
Enabling virtual threads in a Spring Boot application has two prerequisites:
a. Java Version 19 or Later
Virtual threads are a preview feature introduced in Java 19 and officially released in Java 21. If you are using Java 19, ensure you enable the preview feature using the following Maven configuration in your pom.xml
:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>19</source>
<target>19</target>
<compilerArgs>--enable-preview</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
pom.xmlThis configuration allows your project to use Java 19 with preview features enabled, including virtual threads. However, if you are using Java 21 or a later version, this configuration is not required as virtual threads are officially released and enabled by default.
b. Spring Framework Version 6 or Spring Boot Version 3
Utilize Spring Framework version 6 or Spring Boot version 3 (or later versions) to access and leverage the virtual thread features within your Spring application.
5. Enabling Virtual Threads in Spring Boot 3.2 and Above
In Spring Boot 3.2 and later versions, enabling virtual threads is straightforward. You can enable virtual threads by adding the following configuration to your application.properties
file:
spring.threads.virtual.enabled=true
application.propertiesThis simple configuration instructs Spring Boot to leverage virtual threads for concurrency, enhancing the performance of your application without any extra effort.
6. Enabling Virtual Threads in Spring 6 and Spring Boot 3
In lower versions of Spring, such as Spring 6 and Spring Boot 3, you can enable virtual threads in applications running on Tomcat by following this configuration. Ensure you are using at least Java version 19.
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import java.util.concurrent.Executors;
@Configuration
public class ApplicationConfiguration {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
ApplicationConfiguration.javaCode Explanation:
a. applicationTaskExecutor Bean
This bean creates an AsyncTaskExecutor
using TaskExecutorAdapter
to adapt the virtual thread executor created by Executors.newVirtualThreadPerTaskExecutor()
. The newVirtualThreadPerTaskExecutor()
creates a virtual thread executor that spawns a new virtual thread for each task, optimizing concurrency in Java applications. It essentially provides a way to execute tasks asynchronously using virtual threads.
If you do not create theapplicationTaskExecutor
bean, the@Async
annotated code in your application won’t be executed by virtual threads.
b. protocolHandlerVirtualThreadExecutorCustomizer Bean
The TomcatProtocolHandlerCustomizer
is an interface provided by Spring Boot that allows customization of the Tomcat protocol handler. In simpler terms, it lets you configure how Tomcat handles incoming requests and manages its internal processing.
When you implement the TomcatProtocolHandlerCustomizer
interface and provide a custom implementation, you gain control over aspects such as thread management, request handling, and protocol-specific configurations within Tomcat.
The protocolHandlerVirtualThreadExecutorCustomizer
bean customizes the Tomcat protocol handler by setting its executor to use a virtual thread executor created by Executors.newVirtualThreadPerTaskExecutor()
. The newVirtualThreadPerTaskExecutor()
creates a virtual thread executor that spawns a new virtual thread for each task, optimizing concurrency in Java applications. This ensures that tasks handled by Tomcat are executed using virtual threads for improved concurrency.
If you do not create the protocolHandlerVirtualThreadExecutorCustomizer
bean, tasks related to the Tomcat server’s protocol handling, such as handling incoming HTTP requests, will not be executed by virtual threads.
7. Examining Execution Modes of Threads
Now, we’ll look at different scenarios to see if tasks are handled by virtual threads or platform threads. Before examining the thread execution modes, let’s understand our Spring Boot application and the endpoints it offers.
import com.bootcamptoprod.service.RequestProcessingService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@RestController
public class SpringBootVirtualThreadsController {
private final Logger log = LoggerFactory.getLogger(SpringBootVirtualThreadsController.class);
private final RequestProcessingService requestProcessingService;
public SpringBootVirtualThreadsController(RequestProcessingService requestProcessingService) {
this.requestProcessingService = requestProcessingService;
}
@GetMapping("/hello")
public String sendGreetings() {
log.info("Hello endpoint. current thread: {}", Thread.currentThread());
return "Hello, World!";
}
@GetMapping("/hello-with-delay")
public String sendGreetingsWithDelay() throws InterruptedException {
log.info("Hello endpoint with delay. current thread: {}", Thread.currentThread());
return requestProcessingService.greetingsWithDelay();
}
@GetMapping("/hello-async")
public CompletableFuture<String> sendGreetingsAsync() {
log.info("Hello endpoint with async. current thread: {}", Thread.currentThread());
return requestProcessingService.greetingsWithAsyncProcessing();
}
}
SpringBootVirtualThreadsController.javaimport org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
public class RequestProcessingService {
private final Logger log = LoggerFactory.getLogger(RequestProcessingService.class);
@Async
public CompletableFuture<String> greetingsWithAsyncProcessing() {
log.info("Async request processing. current thread: {}", Thread.currentThread());
return CompletableFuture.completedFuture("Hello, World!");
}
public String greetingsWithDelay() throws InterruptedException {
log.info("Request processing with delay. current thread: {}", Thread.currentThread());
Thread.sleep(3000);
return"Hello, World!";
}
}
RequestProcessingService.javaIn our Spring Boot application, we have a controller named SpringBootVirtualThreadsController
and a corresponding service class RequestProcessingService
. The controller contains three endpoints: /hello
, /hello-with-delay
, and /hello-async
. These endpoints demonstrate different scenarios of thread execution in a Spring Boot application.
/hello
– This endpoint is a basic GET request that returns a simple “Hello, World!” message./hello-with-delay
– This endpoint simulates a delay by invoking a method in theRequestProcessingService
that sleeps for 3 seconds.-
/hello-async
– This endpoint demonstrates asynchronous processing by calling an asynchronous method in theRequestProcessingService
.
Let’s explore various scenarios to observe how different thread execution modes are utilized within our application:
Scenario 1: Global Virtual Threads
In this scenario, the virtual threads are enabled at the global level using the Sprig Boot application properties:
spring.threads.virtual.enabled=true
application.properties/hello
: The controller logs display the current thread as avirtual thread
./hello-with-delay
: Both in the controller and service class, the current thread is avirtual thread
./hello-async
: In the controller, the current thread is avirtual thread
, and async processing is executed by avirtual thread
.
Scenario 2: Virtual Threads for AsyncTaskExecutor Only
In the following scenario, virtual threads are enabled specifically for the AsyncTaskExecutor
bean, allowing virtual thread execution for asynchronous tasks only:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import java.util.concurrent.Executors;
@Configuration
public class ApplicationConfiguration {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
}
ApplicationConfiguration.java/hello
: The controller logs display the current thread as aplatform thread
./hello-with-delay
: Both in the controller and service class, the current thread is aplatform
thread./hello-async
: In the controller, the current thread is aplatform thread
, and async processing is executed by avirtual thread
.
Scenario 3: Virtual Threads for TomcatProtocolHandlerCustomizer Only
In this scenario, virtual threads are enabled specifically for the TomcatProtocolHandlerCustomizer
bean, allowing virtual thread execution only for tasks managed by Tomcat’s protocol handler:
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
@Configuration
public class ApplicationConfiguration {
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
ApplicationConfiguration.java/hello
: The controller logs display the current thread as avirtual thread
./hello-with-delay
: Both in the controller and service class, the current thread is avirtual thread
./hello-async
: In the controller, the current thread is avirtual thread
, and async processing is executed by aplatform thread
in the service class.
Scenario 4: Virtual Threads for AsyncTaskExecutor and TomcatProtocolHandlerCustomizer
In this scenario, virtual threads are enabled specifically for the AsyncTaskExecutor
bean and the TomcatProtocolHandlerCustomizer
bean, allowing virtual thread execution for asynchronous tasks and tasks managed by Tomcat’s protocol handler:
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.support.TaskExecutorAdapter;
import java.util.concurrent.Executors;
@Configuration
public class ApplicationConfiguration {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
ApplicationConfiguration.java/hello
: The controller logs display the current thread as avirtual thread
/hello-with-delay
: Both in the controller and service class, the current thread is avirtual thread
./hello-async
: In the controller, the current thread is avirtual thread
, and async processing is executed by avirtual thread
in the service class.
Scenario 5: Virtual Threads Disabled
In this scenario, virtual threads are disabled, ensuring that traditional threads are used throughout the application
/hello
: The controller logs display the current thread as aplatform thread
./hello-with-delay
: Both in the controller and service class, the current thread is aplatform thread
./hello-async
: In the controller, the current thread is aplatform thread
, and async processing is executed by aplatform thread
in the service class.
8. Performance Comparison: Spring Boot App with vs. without Virtual Threads
To evaluate the performance of our Spring Boot application effectively, we’re utilizing JMeter’s response time graphs and Apache Benchmark. Specifically, we’re concentrating on the /hello-with-delay
endpoint, which includes a deliberate three-second delay before responding. This approach allows us to thoroughly analyze response times and system behavior providing valuable insights into performance optimization.
Now, let’s delve into performance comparisons with and without virtual threads.
JMeter Response Time Graph Comparison
JMeter Thread Group Configuration
- Number of Threads: 1000
- Ramp-up Period: 2 seconds
- Loop Count: Infinite
- Thread Lifetime: 120 seconds
Results with Virtual Threads Disabled:
In this scenario, where virtual threads were disabled, the response time graph in JMeter showed varying response times ranging from 8000 milliseconds to a maximum of 16000 milliseconds. This fluctuation indicates that without virtual threads, the application struggled to handle the concurrent requests efficiently, resulting in longer response times as the load increased.
Results with Virtual Threads Enabled:
When virtual threads were enabled, the response time graph in JMeter exhibited a consistent response time of around 3000 milliseconds. This stable response time indicates that virtual threads improved concurrency management within the application, allowing it to handle the same load more efficiently and maintain a lower and more stable response time across the board.
In summary, enabling virtual threads significantly reduced the response time variability and improved the overall responsiveness of the Spring Boot application under high concurrent load conditions.
Apache Benchmark Performance Analysis
We assessed the performance of a Spring Boot application using Apache Benchmark with 2000 requests and 400 concurrent connections against the /hello-with-delay
endpoint. Below are the results comparing performance with virtual threads enabled and disabled:
With Virtual Threads Enabled:
- Time taken for tests: 19.141 seconds
- Requests per second: 104.49 [#/sec] (mean)
- Time per request: 3828.165 [ms] (mean)
- Percentage of requests served within a certain time (ms): 50% at 3011ms, 95% at 3159ms, 99% at 4062ms
With Virtual Threads Disabled:
- Time taken for tests: 33.244 seconds
- Requests per second: 60.16 [#/sec] (mean)
- Time per request: 6648.791 [ms] (mean)
- Percentage of requests served within a certain time (ms): 50% at 6021ms, 95% at 8981ms, 99% at 9051ms
The results clearly demonstrate the impact of virtual threads on performance, with significantly improved throughput and reduced response times under high concurrency scenarios.
9. Addressing Premature Process Termination in Non-Web Spring Boot Applications
In non-web Spring Boot applications where schedulers are utilized, enabling virtual threads can sometimes lead to an unexpected issue. Specifically, the process might terminate prematurely after executing scheduled tasks just once. This behavior can disrupt the expected flow of continuous task execution, causing inconvenience and potentially affecting application functionality.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
@EnableScheduling
@SpringBootApplication
public class DemoApplication {
private final Logger log = LoggerFactory.getLogger(DemoApplication.class);
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Scheduled(fixedRate = 1000)
public void scheduledTask() {
log.info("This is sample scheduled task");
}
}
DemoApplication.javaSolution:
To tackle this problem and ensure the uninterrupted execution of scheduled tasks in non-web Spring Boot applications, a straightforward solution exists. By setting the property spring.main.keep-alive=true
, the application’s lifecycle management is adjusted. This configuration change effectively prevents premature process termination, thereby maintaining the continuous execution of scheduled tasks as intended.
spring.threads.virtual.enabled=true
spring.main.keep-alive=true
application.properties10. Source Code
The complete source code of the above examples can be found here.
Additionally, the Postman collection and JMeter Test Plan are available in the GitHub repository under the resources folder.
a. Postman Collection Path: src > main > resources > postman > Spring-Boot-Virtual-Threads.postman_collection.json
b. JMeter Test Plan Path: src > main > resources > jmeter > Spring-Boot-Virtual-Threads.jmx
11. Things to Consider
When working with Spring Boot virtual threads, it’s important to keep the following considerations in mind:
- Performance Monitoring: Virtual threads can significantly affect performance metrics such as response times, throughput, and resource consumption. It’s crucial to monitor these metrics closely.
- Compatibility with Libraries: Verify the compatibility of third-party libraries and frameworks with virtual threads to avoid potential compatibility issues or performance degradation.
- Exception Handling: Ensure robust exception handling mechanisms are in place to manage exceptions and errors that may arise during the code execution.
12. FAQs
How do I enable virtual threads in my Spring Boot application?
To enable virtual threads, you can set the property spring.threads.virtual.enabled=true in your Spring Boot application properties or configuration.
Can virtual threads help improve scalability and responsiveness in Spring Boot applications?
Yes, virtual threads can contribute to improved scalability and responsiveness by efficiently handling concurrent tasks, reducing thread overhead, and optimizing resource utilization, especially in scenarios with high concurrency requirements.
13. Conclusion
In summary, Spring Boot’s virtual threads offer a great way to boost app performance. By testing thoroughly, developers can make their apps more responsive and scalable. Embracing virtual threads can lead to better app capabilities without compromising stability.
14. Learn More
Interested in learning more?
Check out our blog on Resilience4j Bulkhead: Building Robust Services with Concurrency Limits
Add a Comment