1. Introduction

In this short tutorial, we’ll see how it is possible to leverage the great power of virtual threads in a Spring Boot Application.

Virtual threads are a preview feature of Java 19, which means that they will be included in an official JDK release within the next 12 months. Originally introduced by Project Loom, Spring 6 release grants developers the option to start experimenting with this awesome feature.

Firstly, we’ll see the main difference between a “platform thread” and a “virtual thread”. Next, we’ll build a Spring-Boot application from scratch using virtual threads. Finally, we’ll create a small testing suite to see eventual improvement in the throughput of a simple web app.

2. Virtual Threads vs. Platform Threads

The main difference is that virtual threads do not rely on the OS thread during its cycle of operation: they are decoupled from the hardware, hence the word “virtual”. This decoupling is granted by an abstraction layer provided by the JVM.

For this tutorial, it is essential to grasp that virtual threads are far cheaper than platform threads to operate. They consume way less amount of memory to be allocated. This is why it’s possible to create millions of virtual threads without having out-of-memory problems, rather than the few hundred we can create with standard platform (or kernel) threads.

Theoretically, this grants developers a superpower: managing heavily scalable applications without relying on asynchronous code.

3. Using Virtual Threads in Spring 6

Starting from Spring Framework 6 (and Spring Boot 3), the virtual-thread feature is officially in general availability, but virtual threads are a preview feature of Java 19. This means we need to tell the JVM we want to enable them in our application. Since we are using Maven to build our application, we want to make sure to include the following code in the 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>

From the Java point of view, to work with Apache Tomcat and virtual threads, we need a simple configuration class with a couple of beans:

@EnableAsync
@Configuration
@ConditionalOnProperty(
  value = "spring.thread-executor",
  havingValue = "virtual"
)
public class ThreadConfig {
    @Bean
    public AsyncTaskExecutor applicationTaskExecutor() {
        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }

    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

The first Spring Bean, ApplicationTaskExecutor, will replace the standard ApplicationTaskExecutor providing an Executor that starts a new virtual thread for each task. The second bean, named ProtocolHandlerVirtualThreadExecutorCustomizer, will customize the standard TomcatProtocolHandler in the same way. We have also added the annotation @ConditionalOnProperty to enable virtual threads on demand by switching the value of a configuration property in the application.yaml file:

spring:
    thread-executor: virtual
    //...

Let’s test whether the Spring Boot Application uses virtual threads to handle web request calls. To do this, we need to build a simple controller that returns the required information:

@RestController
@RequestMapping("/thread")
public class ThreadController {
    @GetMapping("/name")
    public String getThreadName() {
        return Thread.currentThread().toString();
    }
}

The toString() method of the Thread object will return all the information we need: the Thread Id, Thread Name, Thread Group, and Priority. Let’s hit this endpoint with a curl request:

$ curl -s http://localhost:8080/thread/name
$ VirtualThread[#171]/runnable@ForkJoinPool-1-worker-4

As we can see, the response explicitly says that we are using a virtual thread to handle this web request. In other words, the Thread.currentThread() call returns an instance of the virtual thread class. Let’s now see the effectiveness of a virtual thread with a simple but effective load test.

4. Performance Comparison

For this load test, we’ll use JMeter. This won’t be a complete performance comparison between Virtual Threads and Standard Threads, but a starting point from which we can build additional tests with different parameters.

In this peculiar scenario, we’ll call an endpoint in a Rest Controller that will simply put the execution to sleep for one second, simulating a complex asynchronous task:

@RestController
@RequestMapping("/load")
public class LoadTestController {

    private static final Logger LOG = LoggerFactory.getLogger(LoadTestController.class);

    @GetMapping
    public void doSomething() throws InterruptedException {
        LOG.info("hey, I'm doing something");
        Thread.sleep(1000);
    }
}

Remember that thanks to the @ConditionalOnProperty annotation, we can switch between virtual threads and standard threads by only changing the value of the variable in the application.yaml.

The JMeter test will contain only one thread group, simulating 1000 concurrent users hitting the /load endpoint for 100 seconds:

JMeter Thread Group

 

The performance gains from adopting this new feature are evident in this case. Let’s compare the “Response Time Graph” of the different implementations. This is the response graph of standard threads. As we can see, the time needed to finish a call quite immediately reaches 5000 milliseconds:

Standard Threads Performace

 

This is happening because platform threads are a limited resource, and when all the scheduled and pooled threads are busy, there is nothing left for the Spring App to do than put the request on hold until one thread is free.

Let’s see instead what happens with virtual threads:

Virtual Threads Graph

 

As we can see, the response settles down at 1000 milliseconds. Virtual threads are created and used immediately after the request because they are super cheap from the resource point of view. In this case, we are comparing the usage of the spring default fixed standard thread pool (which is by default at 200) and the spring default unbounded pool of Virtual Threads.

This kind of performance gain is only possible because the scenario is simplistic and doesn’t consider the whole spectrum of what a Spring Boot application can do. Adopting this abstraction from the underlying OS infrastructure can be a benefit, but not in every case.

5. Conclusion

In this article, we have seen how it is possible to use virtual threads in a Spring 6-based application.

As always, the code is available on GitHub.

Course – LS (cat=Spring)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> THE COURSE
res – REST with Spring (eBook) (everywhere)
4 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are closed on this article!