什么是虚拟线程
虚拟线程是Java 19开始增加的一个特性,与Golang的协程类似。对于一个早已在其他语言中普及、并且如此实用高效的特性,很多Java开发者早已翘首以盼。
虚拟线程和普通线程的区别
从字面意思理解,“虚拟”线程意味着它并非真正的操作系统线程。它不直接由操作系统内核调度,而是由JVM提供的一层线程抽象,并由平台线程(即普通操作系统线程)进行调度。因此,一个平台线程可以承载成千上万个虚拟线程。
虚拟线程的资源消耗比平台线程小得多。在内存充足的情况下,我们可以轻松创建上百万个虚拟线程,这在Java 19之前是不可想象的。
其实,使用过Akka框架的开发者可能会发现,两者在概念上颇有相似之处。只不过Akka的“Actor”模型是由应用层处理,而虚拟线程是由JVM直接支持,在使用上更为简洁和方便。
SpringBoot使用虚拟线程
接下来,我们将在SpringBoot项目中实践虚拟线程,将其应用于异步任务执行器和HTTP请求处理器,以替换默认的线程池。之后,我们将通过对比测试,直观感受虚拟线程与传统线程在性能上的巨大差异。
配置
本次实践环境为:Java版本 java-20.0.2-oracle,SpringBoot版本 3.1.2。
在SpringBoot中启用虚拟线程非常简单,只需添加如下配置类:
/**
* 该配置用于后续测试,spring.virtual-thread=true时使用虚拟线程,false时使用默认的普通线程
*/
@Configuration
@ConditionalOnProperty(prefix = "spring", name = "virtual-thread", havingValue = "true")
public class ThreadConfig {
@Bean
public AsyncTaskExecutor applicationTaskExecutor() {
return new TaskExecutorAdapter(Executors.newVirtualThreadPerTaskExecutor());
}
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
@Async性能对比
我们首先编写一个异步Service,其中模拟一个耗时约50ms的IO操作(如数据库或缓存查询):
@Service
public class AsyncService {
/**
* @param countDownLatch 用于并发测试的计数器
*/
@Async
public void doSomething(CountDownLatch countDownLatch) throws InterruptedException {
Thread.sleep(50);
countDownLatch.countDown();
}
}
然后,我们编写测试方法,循环调用该异步方法10万次,并计算总耗时:
@Test
public void testAsync() throws InterruptedException {
long start = System.currentTimeMillis();
int n = 100000;
CountDownLatch countDownLatch = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
asyncService.doSomething(countDownLatch);
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
}
普通线程耗时:约678秒(超过10分钟)。

虚拟线程耗时:约3.9秒!

可以看到,性能差距接近200倍,这无疑是质的变化。传统线程池在面对海量短期阻塞任务时,多线程的创建和上下文切换开销巨大,而虚拟线程则能轻装上阵。
HTTP请求性能对比
我们再看看在Web场景下的表现。编写一个简单的GET接口,同样模拟50ms的IO延迟:
@RequestMapping("/get")
public Object get() throws Exception {
Thread.sleep(50);
return "ok";
}
接下来,我们使用JMeter进行压测:设置500个并发线程,循环执行,总请求数为1万次。
「普通线程表现:」

从图中可以看到,最小响应时间约为50ms,这符合接口内的睡眠设置。但是,中位数以及90%、95%、99%分位的响应时间都超过了150ms。这是因为平台线程是昂贵的系统资源,SpringBoot内嵌Tomcat的默认最大连接数通常为200。当这200个线程都在“干等”50ms时,后续的请求无法得到处理,只能在队列中排队,从而导致响应时间大幅增加。
「虚拟线程表现:」

切换到虚拟线程后,即使是最大响应时间也保持在100ms以下。这意味着线程等待时间显著减少,系统资源(特别是CPU)得到了更充分的利用,能够以更低的延迟处理更多的并发请求。
总结
通过上述对比测试,虚拟线程在性能上的优势非常明显。但需要特别注意:我们的测试都让线程等待了50ms,这模拟的是什么场景?
没错,正是IO密集型场景。在这种场景下,线程大部分时间在等待网络、磁盘等IO操作,而非执行计算。虚拟线程的优势在于,当某个虚拟线程因IO而阻塞时,它可以立即让出承载它的平台线程,去执行其他就绪的虚拟线程,从而极大地提升了平台的线程利用率。
如果是CPU密集型任务(线程大部分时间在进行计算),虚拟线程带来的提升可能有限。不过,当前绝大多数Web应用都属于IO密集型,充斥着数据库查询、缓存访问、外部API调用等操作。因此,在Spring Boot应用中启用虚拟线程,对于提升系统吞吐量和降低延迟具有显著意义。
最后,虽然很多公司可能仍在使用Java 8,但技术进步的车轮从未停歇。是时候考虑升级,拥抱像虚拟线程这样能带来实质性提升的新特性了。如果你想了解更多关于Java新特性或高并发架构的实践经验,欢迎来云栈社区交流探讨。