本文探究了虚拟线程和Project Loom的概念,以及在spring应用中进行实践,最后对其性能进行了测试。
在我以前的银行项目中,我们曾遇到过这样的情况:我们收到了成千上万的认证请求。为了确保安全,我们依靠第三方系统来发送短信OTP进行验证。然而,有时第三方系统确认请求的时间比预期的要长,造成了延迟。我们使用了每个请求一个线程的模型,这意味着有许多线程处于等待状态,而新的请求则堆积在池中。我们试图通过微调线程的数量来优化性能,我希望我们当时有虚拟线程的功能。在当时,Java中还没有虚拟线程的概念。这就是Project Loom发挥作用的地方。
在这篇文章中,我们将探究以下内容:
- 什么是Project Loom?
- 如何使用虚拟线程?
- 虚拟线程和Spring MVC
- 使用和不使用虚拟线程的性能测试比较
什么是 Project Loom?
Project Loom 作为一个有用的工具,旨在简化并发编程的复杂性。它的主要目标是使你更容易编写、维护和观察需要同时处理多个任务的应用程序。它通过引入一个叫做 "虚拟线程(virtual threads) "的概念来实现这一目标。
那么,这些虚拟线程究竟是什么?它们是一种智能解决方案,可以提高你的应用程序的性能和可扩展性。它们是专门为你遇到我们所说的 “阻塞性API” 的情况而设计的。这些是你的代码的一部分,可能会阻塞并等待一些事情的发生,例如等待来自数据库或外部服务的响应。
现在,令人兴奋的部分来了:虚拟线程计划被纳入下一版本的 Java Development Kit (JDK 21)。这意味着它们也可能出现在即将发布的Spring框架6.1中,该框架计划于今年晚些时候发布。作为 Spring 的开发者,掌握虚拟线程的概念并理解为什么它们在我们的开发过程中意义重大,对我们来说至关重要。
如何使用虚拟线程
让我们用Java 20(或Java 19)和Spring Boot 3.1创建一个简单的Spring Boot MVC应用程序。该应用程序将有一个名为 “/home” 的端点,以预定义的时间延迟返回线程的名称。这个演示的代码可以在github上找到。
创建项目,开启预览特性
使用 Spring Initializer 或你的IDE创建项目。添加 spring-web 的依赖并添加元数据。当你在IDE中打开项目时,确保你按下面的方法更新 pom.xml
,以确保 预览特性 被启用,而 预览特性 在默认情况下是禁用的。
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<compilerArguments>--enable-preview</compilerArguments>
</configuration>
</plugin>
创建 Controller 和Service 类
创建名为 HomeController
和 HomeService
类,如下所示:
@RestController
@RequestMapping("/")
public class HomeController {
@Autowired
HomeService homeService;
@GetMapping("/home")
public String getResponse(){
return homeService.getResponse();
}
}
@Service
public class HomeService {
private static final Logger LOGGER = LoggerFactory.getLogger(HomeService.class);
//This method will add delay in execution and return name of thread
public String getResponse(){
//Adding sleep
int sleepTime = 250 ; //new Random().nextInt(1000); -- Uncomment the line if you want to add random delay
try {
TimeUnit.MILLISECONDS.sleep(sleepTime);
} catch (InterruptedException e) {
LOGGER.error( e.getMessage());
}
return "Current Thread Name: " + Thread.currentThread().toString();
}
}
测试应用
这将创建没有虚拟线程的简单 rest 服务,让我们测试一下,看看我们是否能使用postman或浏览器来运行。
让我们现在通过在 Application main class 中添加以下内容来启用虚拟线程:
@Bean
TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
这个 Bean 是 Tomcat protocol handler 的定制器。Tomcat protocol handler 负责处理Spring Boot应用程序中的传入请求。这个自定义器的目的是配置 protocol handler 所使用的 executor 。
在 protocolHandlerVirtualThreadExecutorCustomizer
方法中,一个lambda表达式被用来定制 protocol handler。protocolHandler
参数表示被定制的 Tomcat protocol handler 的实例。
在lambda表达式中,调用了 protocolHandler
对象的setExecutor()
方法。这个方法被用来设置 protocol handler 的 executor 。一个 executor 负责执行任务,比如处理传入的请求。
在这种特定情况下,Executors.newVirtualThreadPerTaskExecutor()
方法被用来创建一个使用虚拟线程的新executor。虚拟线程是轻量级的线程,可以在单个操作系统线程中并发地执行任务。这意味着可以同时执行多个任务,提高性能和资源利用率。
重新启动应用程序并再次测试,你会看到现在的虚拟线程名称如下所示。
性能测试的比较
我用Gatling测试了这个应用程序。Gatling是一个流行的负载测试工具,可以用来模拟用户行为,测试Spring Boot应用程序在不同程度的负载和压力下的性能。
我对该应用程序进行了测试,使用了四个场景:一个是sleep 时间为100ms,另一个是sleep 时间为250ms。我测试了有和没有虚拟线程的两种情况。在测试过程中,我模拟了100个用户并发地向端点发送请求,每个用户发出50个请求,总共有5000个请求。
可以在 Gatling UI 中查看报告,如下所示:
在100ms的延迟下,下面是测试的总结。所有的响应时间都是以ms为单位。
在250ms的延迟下,下面是测试的总结。
总结
在这篇博客中,我们探讨了虚拟线程和Project Loom的概念。我们发现,虚拟线程对于处理阻塞性操作特别有利。为了将虚拟线程付诸实践,我们创建了一个Spring MVC应用程序,并利用了Java 19/20中的虚拟线程预览功能。这使我们能够在我们的应用程序中利用虚拟线程,并获得其潜在的好处。
为了评估虚拟线程对应用程序性能的影响,我们进行了性能负载测试。这些测试包括让我们的应用程序承受大量的并发用户和请求,看看它是如何处理工作负载的。通过比较有无虚拟线程的结果,我们可以评估利用虚拟线程所带来的性能改进。