Spring Boot Virtual Threads

Spring Boot Virtual Threads Explained: Easy Configuration & Examples

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.xml

This 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.properties

This 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.java

Code 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 the applicationTaskExecutor 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.java
import 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.java

In 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.

  1. /hello – This endpoint is a basic GET request that returns a simple “Hello, World!” message.
  2. /hello-with-delay – This endpoint simulates a delay by invoking a method in the RequestProcessingService that sleeps for 3 seconds.
  3. /hello-async – This endpoint demonstrates asynchronous processing by calling an asynchronous method in the RequestProcessingService.


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 a virtual thread.
  • /hello-with-delay: Both in the controller and service class, the current thread is a virtual thread.
  • /hello-async: In the controller, the current thread is a virtual thread, and async processing is executed by a virtual 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 a platform thread.
  • /hello-with-delay: Both in the controller and service class, the current thread is a platform thread.
  • /hello-async: In the controller, the current thread is a platform thread, and async processing is executed by a virtual 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 a virtual thread.
  • /hello-with-delay: Both in the controller and service class, the current thread is a virtual thread.
  • /hello-async: In the controller, the current thread is a virtual thread, and async processing is executed by a platform 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 a virtual thread
  • /hello-with-delay: Both in the controller and service class, the current thread is a virtual thread.
  • /hello-async: In the controller, the current thread is a virtual thread, and async processing is executed by a virtual 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 a platform thread.
  • /hello-with-delay: Both in the controller and service class, the current thread is a platform thread.
  • /hello-async: In the controller, the current thread is a platform thread, and async processing is executed by a platform 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.java

Solution:

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.properties


10. 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:

  1. 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.
  2. Compatibility with Libraries: Verify the compatibility of third-party libraries and frameworks with virtual threads to avoid potential compatibility issues or performance degradation.
  3. 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?

Can virtual threads help improve scalability and responsiveness in Spring Boot applications?

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

Your email address will not be published.