posts
A guide to using virtual threads with Spring Boot

A guide to using virtual threads with Spring Boot

Dec 14, 2023
Catherine Edelveis
7.2

We’ve been looking forward to it for a very long time — virtual threads were finalized in JDK 21! The feature is now complete and production-ready. We discussed the functionality in more detail in a dedicated article, and now, let’s see how we can use virtual threads with new Spring Boot 3.2!

By the way, if you deploy Spring Boot services to the cloud, check out Alpaquita Containers tailor-made for Spring Boot: they can help you save up to 30 % RAM!

And did you know that you can easily reduce startup and warmup times of your Spring Boot services from minutes to milliseconds by using CRaC API for Java? Give it a try!

Why are virtual threads so important for Java development?

There’s a lot of buzz around virtual threads, but are they really that beneficial for multithreaded Java applications?

Virtual threads are not a magic pill for all cases of concurrent applications: everything depends on performance goals. The feature doesn’t improve latency (meaning that it doesn’t increase the speed of operation), but it is highly beneficial for high-throughput server applications that handle a lot of requests associated with blocking I/O calls.

Therefore, virtual threads enable the developers to scale the application maintaining the optimal hardware utilization and preserving the traditional thread-per-request style that simplifies the writing, maintaining, and controlling multithreaded code as opposed to reactive programming.

How to use virtual threads with Spring Boot

For virtual threads to work, you need JDK 21 (for example, Liberica JDK, the default runtime for Spring. You can get a build for your platform here) and Spring Boot 3.2.

Add the following property to the application.properties file to enable virtual threads in your application:

spring.threads.virtual.enabled=true

This is all you need to implement the feature. This setting works both for the embedded Tomcat and Jetty.

If you use an older Spring Boot version (for instance, 3.1), you can create the following configuration to work with the embedded Tomcat:

@EnableAsync
@Configuration
public class VirtualThreadConfig {
    @Bean
    public AsyncTaskExecutor applicationTaskExecutor() {
        return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
    }

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

Note that virtual threads don’t substitute all standard threads magically. These settings are relevant for asynchronous operations with Spring MVC and Spring Web Flux because the framework is adapted to virtual threads. For instance, this functionality is used to process @Async methods when the @EnableAsync is enabled.

Measuring the performance of virtual threads

Let’s measure the performance improvement (if any) using a benchmark available on GitHub, and run it with mvn clean install.

This benchmark starts up a simple application for Spring Boot 3.2 and Java 21, whose main controller upon receiving a request, waits for 300 milliseconds before giving an ID of a current thread (the GitHub code is here):

public class VirtualThreadController {
    private static final Logger LOGGER = LoggerFactory.getLogger(VirtualThreadController.class);
    public static final int SLEEP_TIME = 300;

    @GetMapping("/")
    public String getResponse(){
        try {
            TimeUnit.MILLISECONDS.sleep(SLEEP_TIME);
        } catch (InterruptedException e) {
            LOGGER.error(e.getMessage());
        }

        long threadId = Thread.currentThread().threadId() ;
        return  String.valueOf(threadId);
    }
}

The application performance is measured with a Gatling-based script written in Scala (the GitHub code is here):

class VirtualThreadSimulation extends Simulation {

  before {
    val app = SpringApplication.run(classOf[VirtualThreadsApplication])
    app.registerShutdownHook()
  }

  val httpProtocol = http
    .baseUrl("http://localhost:8080")
    .acceptHeader("application/json")
    .contentTypeHeader("application/json")

  val vtScenario = scenario("Virtual Thread Scenario").repeat(1000) {
    exec(http("Call the Controller")
      .get("/")
      .check(status.is(200)))
  }

  setUp(
    vtScenario.inject(atOnceUsers(500))
  ).protocols(httpProtocol)

}

This script simulates the situation when 500 users send 1.000 requests to the controller simultaneously.

You can set the virtual threads configuration in application.properties. The first property enables the standard configuration from Spring Boot. The second one enables our Bean with a manually configured AsyncTaskExecutor.

spring.threads.virtual.enabled=true
spring.threads.virtual.enabled.manually=false

There’s currently no difference between these parameters, but in the future, the Spring Boot configuration may differ from the old manual configuration.

You can view the results in the HTML format in the browser:

Virtual threads benchmarking results

These reports demonstrate that the results depend heavily on the hardware.

Therefore, this test can serve as a foundation for experiments. The number may be increased or decreased depending on the capacities of your computer. For instance, meaningful results for a powerful 64-core server machine will require a bigger load than that for a laptop with 4 cores.

In addition, you can utilize different load patterns. For example, you can substitute a simple sleep with real database queries or add a computation load that will use CPU resources and fill the RAM with big data.

I tested the load of 500 users and 1.000 requests on Windows running on AMD Ryzen 9 3950X 16-Core Processor with 8Gb of RAM. Virtual threads gave an approx. 2-fold improvement as compared to standard threads.

So, we switched one setting in Spring Boot and got a significant performance improvement “for free.” In my opinion, this qualifies as a good result.

Speaking of performance gains, there’s a simple way to boost the performance of your containerized Spring Boot apps, Alpaquita Containers. Based on Liberica Lite and a lightweight Alpaquita Linux, they can reduce the memory footprint of containers by up to 30%!

Subcribe to our newsletter

figure

Read the industry news, receive solutions to your problems, and find the ways to save money.

Further reading