环境:SpringBoot3.2.5 + JDK21
1. 简介
从JDK21开始正式发布Java虚拟线程的支持,而SpringBoot从3.2.x开始对虚拟线程支持,列如任务调用,异步任务,异步请求都可以使用虚拟线程。随着越来越多的开发者开始关注其在高并发场景下的性能表现。特别是在Spring Boot这样的主流框架中,如何充分利用虚拟线程来提升异步请求的处理能力,成为了业界探讨的热点话题。本文将以SpringBoot 3.2.5为背景,通过JMeter对使用平台线程和虚拟线程的异步请求进行性能对比,展示虚拟线程在性能方面的优势。
如果你对虚拟线程还不了解请先查看下面这篇文章:
【技术革命】JDK21虚拟线程来袭,让系统的吞吐量翻倍!
什么是异步请求?如果你对异步请求不了解请先查看下面这篇文章
快速掌握Spring异步请求接口,轻松解决并发问题
2. 实战案例
2.1 模拟耗时操作
这里我们先模拟耗时操作,分别通过平台线程和虚拟线程进行性能的测试。
测试接口
// SpringMVC中通过返回Callable进行@GetMapping("/platform")public Callable<Object> platform() {
System.out.printf("请求开始:%d%n", System.currentTimeMillis()) ;
Callable<Object> callable = () -> {
System.out.printf("当前执行线程:%s%n", Thread.currentThread().getName()) ; // 模拟耗时操作
TimeUnit.SECONDS.sleep(2) ; return "Callable耗时操作完成" ;
} ;
System.out.printf("请求结束:%d%n", System.currentTimeMillis()) ; return callable ;
}先通过浏览器测试次接口是否工作正常

控制台输出
请求开始:1613619357014 请求结束:1613619357016 当前执行线程:task-1
通过输出结果看出,虽然接口模拟了2s的耗时操作,但是处理请求的tomcat线程机会瞬间完成(2ms)。通过上面的测试接口异步没有问题。接下来分别通过使用平台线程和虚拟线程进行性能测试。
注意下面的所有测试都会配置如下的JVM参数,保证每次JVM使用的内存都是一致的。
-Xms1g -Xmx1g
同一的JMeter配置如下:

模拟500个并发,循环10次。
请求接口都是上面定义的接口

平台线程①
本次测试不做任何配置,都使用默认值(如:默认异步请求使用的线程池)。
通过几轮的测试,大体平均值如下:

请求中有大量的错误,这些错误都是由于超时缘由(通过下面的控制台输出得知)
控制台输出如下:


通过控制台的输出得出,在默认情况下异步请求任务的执行线程只有8个,这就导致有大量的线程需要等待,而这个默认等待的超时时间是30s,当有大量的tomcat请求过来,但是处理任务的只有8个线程,那么必定出现异步请求超时情况也不奇怪了。
通过上面的测试结果得出在默认情况下起步请求的性能是及其低的而且还会出现大量的错误。接下来通过调整线程数量再进行测试:
spring: task: execution: pool: core-size: 200
将核心线程数设置为200。测试结果如下:

这次测试没有错误了,同时吞吐量差不多100,满共200线程,每个任务模拟的是2s。本次测试结果是通过调整异步任务线程池大小来加快任务的处理,但是这肯定会影响到应用的整体性能(会与Tomcat线程池争夺CPU和内存资源,导致HTTP请求的响应时间增加或吞吐量下降)。
虚拟线程②
将上面的配置都删除,恢复到初始状态,然后开启虚拟线程
spring: threads: virtual: enabled: true
通过上面的配置即可,在进行测试前我们先通过源码来说明下什么情况下使用平台线程或虚拟线程。有一点需要注意:SpringMV的异步请求使用的线程池bean,这个bean的名称必须是:applicationTaskExecutor大家可以通过查看
RequestMappingHandlerAdapter对象的创建得知。在自动配置中有如下代码:


如果没有开启上面的配置,那么就是下面的ThreadPoolTaskExecutor生效,默认的核心线程数为8。接下来使用虚拟线程进行测试结果如下:

好家伙,什么都没有配置吞吐量远远高于平台线程。不过你通过控制台会发现创建了超级多的虚拟线程,不过虚拟线程的成本是比较低的(不是由操作系统管理,消耗更少的CPU和内存)。
如果你是远程接口调用,如下:
@GetMapping("/http")public Callable<Object> http() {
System.out.printf("请求开始:%d%n", System.currentTimeMillis()) ;
Callable<Object> callable = () -> {
System.out.printf("当前执行线程:%s%n", Thread.currentThread().getName()) ; return new RestTemplate().getForObject("http://localhost:8088/demos/rr", String.class) ;
} ;
System.out.printf("请求结束:%d%n", System.currentTimeMillis()) ; return callable ;
}这种情况下测试的结果与上面模拟的效果基本一样。
通过上面的测试用例得出,在此种情况下性能虚拟线程是远远高于平台线程。
2.2 基于数据库测试
准备数据表

10W数据进行测试。
测试接口
publicinterfaceBigTableRepositoryextendsJpaRepository<BigTable, Integer> { // 统计总数 @Query("select count(name) from BigTable e") long count() ;
}数据库连接池配置
spring: datasource: driverClassName: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/batch?serverTimezone=GMT%2B8&useSSL=false username: root password: xxxooo type: com.zaxxer.hikari.HikariDataSource hikari: minimumIdle: 200 maximumPoolSize: 200
JMeter配置还是与上面一样。
平台线程①
spring.task.execution.pool.core-size=8
测试结果

spring.task.execution.pool.core-size=100
测试结果

spring.task.execution.pool.core-size=200
测试结果

结论:线程数逐步加大,系统整体的吞吐量没有太大的变化。
虚拟线程②
使用虚拟线程测试结果如下

使用虚拟线程整体的吞吐量反而下降的超级多。
来自官网的说明:
虚拟线程可以在以下情况下显著提高应用程序的吞吐量
并发任务数量多(超过几千个),而且
该工作负载不受 CPU 限制,由于在这种情况下,线程数量多于处理器内核并不能提高吞吐量。
虚拟线程有助于提高典型服务器应用程序的吞吐量,这正是由于此类应用程序由大量并发任务组成,而这些任务的大部分时间都在等待。
针对虚拟线程的配置,还有如下参数设置
spring: task: execution: simple: concurrency-limit: 100
限制并非执行的数量,默认是没有限制:-1。这是限制并发的数量。