Embracing Virtual Threads in Spring Boot

anil gola
5 min readMay 20, 2023

--

In this blog, we will see how we can take leverage of project loom virtual threads in spring-boot. We will also do some load testing with the help of JMeter and see how response time for both virtual threads and normal threads.

First thing first, Virtual threads are a part of Project Loom.

Also, Loom does not accelerate your in-memory computations, for example parallel stream. This is not a goal of this project.

We are looking at how to increase our application throughput with the same hardware available to us, i.e., using our CPU to its full potential, for which we are spending great bucks. As of now, we are able to utilise 2–3 percent of our CPU. I have discussed this in detail in this blog:

https://medium.com/@anil.java.story/project-loom-virtual-threads-part-1-b17e327c8ba7

“I think Project Loom is going to kill Reactive Programming” — Brian Goetz (Java language architect)

Let’s quickly setup our spring boot project.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.anil</groupId>
<artifactId>virtualthread</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>virtualthread</name>
<description>virtualthread</description>
<properties>
<java.version>20</java.version>
<tomcat.version>11.0.0-M4</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>8.0.33</version>
</dependency>


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
<source>20</source>
<target>20</target>
</configuration>

</plugin>
</plugins>
</build>

</project>

We need to enable preview features since Project Loom is in the preview stage. We have to wait for Java 21’s release for it to become a final feature. The PR has been merged in main branch of JDK. Consider reviewing 100,000 lines of codes. This many lines of code PR was raised, reviewed and merged.

package org.anil.virtualthread;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;

import java.util.concurrent.Executors;

@SpringBootApplication
@Slf4j
public class VirtualthreadApplication {

public static void main(String[] args) {
SpringApplication.run(VirtualthreadApplication.class, args);
}

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
log.info("Configuring " + protocolHandler + " to use VirtualThreadPerTaskExecutor");
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}

}

As of now, we need to configure virtual thread settings for the Tomcat server. In the future, this may be taken care of in auto-configuration itself.

package org.anil.virtualthread;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class HomeController {

@Autowired
ProductRepository productRepository;


@GetMapping("/thread")
public List<Product> checkThread() throws InterruptedException {
Thread.sleep(1000);
return productRepository.findAll();
}


@PostMapping("/save")
public String saveProduct() throws InterruptedException {
for(int i=0; i< 1000; i++){
Product product = new Product();
product.setProductName(RandomStringUtils.randomAlphanumeric(5));
product.setPrice(RandomUtils.nextLong(10,1000));
product.setPrice(1L);
productRepository.save(product);
}
return "anil";
}
}

We have a GetMapping that returns all the products; we have 1000 Products in our database. We have made our thread sleep for 1 second. Let’s see our Product entity and ProductRepository as well.

package org.anil.virtualthread;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Entity
@Getter
@Setter
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String productName;
private Long price;
}
package org.anil.virtualthread;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product,Long> {
}

Let’s see our application.yaml

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
maxIdle: 1
timeBetweenEvictionRunsMillis: 60000
hikari:
connection-timeout: 60000
maximum-pool-size: 10
minimum-idle: 5
url: jdbc:mysql://localhost:3306/todos
testWhileIdle: true
username: root
password: root1234
validationQuery: SELECT 1
flyway:
baseline-version: 0
enabled: true
validate-on-migrate: false
jpa:
database: mysql
generate-ddl: true
hibernate:
ddl-auto: none
format_sql: true
show-sql: true

Now, let’s first run the application by commenting on the following line: This will run our application on normal threads.

package org.anil.virtualthread;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;

import java.util.concurrent.Executors;

@SpringBootApplication
@Slf4j
public class VirtualthreadApplication {

public static void main(String[] args) {
SpringApplication.run(VirtualthreadApplication.class, args);
}

// @Bean
// public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
// return protocolHandler -> {
// log.info("Configuring " + protocolHandler + " to use VirtualThreadPerTaskExecutor");
// protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
// };
// }
}

Now let’s setup our JMeter. We will have 1000 requests, which will ramp up in 3 seconds. And it will continue like this for a duration of 200 seconds. Every 3 seconds, 1000 GET (“/thread”) requests will be fired. We have also added a Response Time Graph Listener.

Now let’s run our test and wait for 200 seconds.

In the graph, we can see that once the entire thread pool of Tomcat is utilised, the response time shoots from 3600 ms to 5200 ms. Since then, it has remained like that only as previous threads are released.

Now let’s run a load test with the virtual thread feature enabled.

package org.anil.virtualthread;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer;
import org.springframework.context.annotation.Bean;

import java.util.concurrent.Executors;

@SpringBootApplication
@Slf4j
public class VirtualthreadApplication {

public static void main(String[] args) {
SpringApplication.run(VirtualthreadApplication.class, args);
}

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
log.info("Configuring " + protocolHandler + " to use VirtualThreadPerTaskExecutor");
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}

}

Now let’s run our test and wait for 200 seconds.

Clearly, now the response time for concurrent 1000 requests is nearly just above 1000 ms and at some point shoots up to 1400 ms, which is far better than when we were using normal threads.

Clearly, when we need to utilise the underlying CPU to the fullest, we should start embracing virtual threads in our application, and suddenly we can see that the throughput of our application has increased manifold for the same hardware.

This is much better than switching to reactive programming, which means rewriting all your code, which is very hard to learn first, then write, and even harder to debug and profile.

In a nutshell, more users can use the application and get their response in the same time as the first user.

For how to create virtual threads in Project Loom, please check

My youtube channel:

Asynchronous Programming : https://youtu.be/AHL2zuZ_5_k

My talks in English : https://youtube.com/playlist?list=PL40GtTTY6_-ksLCcjKcHLALPEq66WHpwO

My talks in Hindi: https://www.youtube.com/playlist?list=PL40GtTTY6_-kB4qcrRLzulRmVtM1Ip4Ij

If you like this blog, please clap and follow our channel. We keep posting interesting topics from Java Ecosystem. Also, you can support our team financially:

https://buy.stripe.com/6oE5m2e4d8jZ4OkbII

--

--