1. 简介
在本篇文章中,我们将学习如何在Spring Boot应用程序中利用虚拟线程的强大功能。
虚拟线程由Project Loom引入,并在Java 19中作为预览功能提供,并且在成为官方JDK 21版本的一部分。此外,Spring 6版本集成了这一强大功能,允许开发者进行尝试。
首先,我们将了解“平台线程”与“虚拟线程”之间的主要区别。接下来,我们将从头开始构建一个使用虚拟线程的Spring Boot应用程序。最后,我们将创建一个小型测试,以检查简单Web应用的吞吐量是否有所提升。
虚拟线程 VS. 平台线程
主要区别在于,虚拟线程在运行周期内不依赖操作系统线程。虚拟线程与硬件解耦,因此称为 "虚拟"。此外,JVM 提供的抽象层赋予了这种解耦。
在本文中,我们要验证虚拟线程的运行成本远低于平台线程。我们要确认,创建数百万个虚拟线程不会出现内存不足错误(平台线程容易出现此问题)。
关于虚拟线程的详细介绍,可查看下面这篇文章
提升系统吞吐量,详解JDK21虚拟线程,炸裂
2. 实战案例
2.1 开启虚拟线程支持
从 Spring Boot 3.2 开始,如果我们使用 Java 21,启用虚拟线程非常简单。我们将 spring.threads.virtual.enabled 属性设置为 true,然后就可以开始了:
spring:
threads:
virtual:
enabled: true
理论上,我们不需要做其他任何事情。但是,从普通线程切换到虚拟线程可能会给传统应用程序带来不可预见的后果。因此,我们必须对应用程序进行全面测试。
2.2 验证虚拟线程
通过上面开启虚拟线程后,我们通过如下方式是否正确的开启了虚拟线程。
@GetMapping("name")
public String toThread() {
return Thread.currentThread().toString() ;
}
这里我们打印当前处理请求的线程名称,输出结果:
图片
响应结果明确指出我们正在使用虚拟线程处理此网络请求。换句话说,Thread.currentThread() 调用返回了 VirtualThread 类的一个实例。
2.3 性能对比
为了比较性能,我们将使用 JMeter 运行负载测试。这并不是一个完整的性能比较,而是一个起点,我们可以从这个起点出发,用不同的参数建立更多的测试。
在这个特定场景中,我们将通过Controller接口进行测试,该接口只需让执行进入休眠状态一秒钟,模拟一个复杂的异步任务:
@RestController
@RequestMapping("/load")
public class LoadTestController {
private static final Logger logger = LoggerFactory.getLogger(LoadTestController.class) ;
@GetMapping
public void test() throws InterruptedException {
logger.info("日志信息...") ;
// 模拟耗时操作
Thread.sleep(1000) ;
}
}
接下来,在JMeter中创建一个线程组,模拟 1000 个并发用户在 100 秒内访问 /load 接口:
图片
在这种情况下,采用这项新功能所带来的性能提升是显而易见的。让我们比较一下不同实现的 "响应时间图"。这是标准线程的响应时间图。我们可以看到,完成一次调用所需的时间很快就达到了 5000 毫秒:
图片
这种情况发生是因为平台线程是一种有限资源。当所有计划的和池中的线程都在忙碌时,Spring 应用程序只能等待,直到有一个线程空闲下来,才能处理该请求。
接下来,使用虚拟线程进行测试
图片
生成的图表显示,响应时间稳定在1000毫秒。因此,从资源消耗的角度来看,虚拟线程非常高效,请求发出后会立即创建并使用它们。
这种性能提升仅在像我们的演示示例这样的简单场景中才可能实现。实际上,对于CPU密集型操作,虚拟线程并不合适,因为这类任务需要极少的阻塞。
下面,我们在通过一个需要CPU大量计算的操作进行测试,测试代码如下:
// 该示例计算大数的阶乘
@GetMapping("calc")
public String calc() {
// 取值越大计算耗时就越高
int number = 20000 ;
// 开始时间
long startTime = System.currentTimeMillis();
System.out.println("开始时间: " + new Date(startTime));
// 执行耗时计算
factorial(number);
// 结束时间
long endTime = System.currentTimeMillis();
System.out.println("结束时间: " + new Date(endTime));
// 计算总耗时
long duration = (endTime - startTime);
return "计算" + number + "! 耗时: " + duration + " 毫秒" ;
}
private static BigInteger factorial(int n) {
BigInteger result = BigInteger.ONE;
for (int i = 1; i <= n; i++) {
result = result.multiply(BigInteger.valueOf(i));
}
return result;
}
首先,是平台线程测试结果如下:
图片
如下,是虚拟线程测试结果
图片
根据这里的测试结果,发现他们的结果差不多。但虚拟线程似乎更加平稳吧。