Spring Boot虚拟线程源码解析:揭秘M:N调度的底层实现

作为Java开发者,想必你早已听说过虚拟线程的大名。自Java 21将其纳入标准特性,Spring Boot 3.2+又对它进行了深度集成,这个JVM层面的轻量级线程,凭借M:N调度模式带来的高并发能力,成了IO密集型场景的性能救星。今天,咱们就透过Spring Boot的源码,一步步拆解虚拟线程M:N调度的底层实现,看看它究竟是如何用少量平台线程,撑起百万级并发的。

一、先搞懂M:N调度到底是什么

在聊源码之前,得先把M:N调度的核心逻辑掰扯清楚。传统平台线程是1:1映射,一个Java线程对应一个操作系统内核线程,创建和切换成本极高,并发上限也就几千级。而虚拟线程的M:N调度,是让M个虚拟线程映射到N个平台线程(N通常远小于M),调度权从操作系统内核转移到了JVM手里。

当某个虚拟线程执行IO操作(比如数据库查询、网络调用)陷入阻塞时,JVM会把它从当前绑定的平台线程上卸载下来,让这个平台线程去执行其他就绪的虚拟线程。等IO操作完成,被阻塞的虚拟线程再重新挂载到空闲的平台线程上继续执行。这样一来,平台线程就不会被白白浪费在等待上,系统吞吐量自然就上去了。

二、Spring Boot开启虚拟线程的入口:一行配置的魔力

Spring Boot 3.2+开启虚拟线程特别简单,只需在application.yml里加一行配置:

spring:

threads:

virtual:

enabled: true

这行配置到底是怎么生效的?咱们从源码里找答案。Spring Boot专门提供了VirtualThreadAutoConfiguration自动配置类,它会在检测到spring.threads.virtual.enabled=true时生效:

@AutoConfiguration

@ConditionalOnClass(Executors.class)

@ConditionalOnProperty(prefix = "spring.threads.virtual", name = "enabled", havingValue = "true")

public class VirtualThreadAutoConfiguration {

@Bean

@ConditionalOnMissingBean

public VirtualThreadsExecutorServiceConfigurer virtualThreadsExecutorServiceConfigurer() {

return new VirtualThreadsExecutorServiceConfigurer();

}

}

这个配置类会注册一个VirtualThreadsExecutorServiceConfigurer,它是Spring Boot集成虚拟线程的核心处理器,负责把原本使用平台线程池的地方,替换成虚拟线程池。

三、核心处理器:VirtualThreadsExecutorServiceConfigurer的秘密

VirtualThreadsExecutorServiceConfigurer实现了BeanPostProcessor接口,能在Bean初始化完成后对其进行修改。它的核心逻辑在postProcessAfterInitialization方法里:

public class VirtualThreadsExecutorServiceConfigurer implements BeanPostProcessor {

@Override

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

if (bean instanceof TomcatProtocolHandlerCustomizer) {

return bean;

}

if (bean instanceof ExecutorService) {

return wrapExecutorService((ExecutorService) bean);

}

if (bean instanceof AsyncTaskExecutor) {

return wrapAsyncTaskExecutor((AsyncTaskExecutor) bean);

}

return bean;

}

private ExecutorService wrapExecutorService(ExecutorService executor) {

if (isPlatformThreadPool(executor)) {

return new VirtualThreadExecutorServiceWrapper(executor);

}

return executor;

}

private AsyncTaskExecutor wrapAsyncTaskExecutor(AsyncTaskExecutor executor) {

if (executor instanceof SimpleAsyncTaskExecutor simpleAsyncTaskExecutor) {

simpleAsyncTaskExecutor.setVirtualThreads(true);

return simpleAsyncTaskExecutor;

}

return new VirtualThreadAsyncTaskExecutorWrapper(executor);

}

}

它会识别容器里的ExecutorService和AsyncTaskExecutor类型的Bean,如果是平台线程池,就用VirtualThreadExecutorServiceWrapper或者VirtualThreadAsyncTaskExecutorWrapper进行包装,把执行任务的逻辑替换成虚拟线程。

四、Tomcat与虚拟线程的集成:Web请求的M:N调度实战

Spring Boot默认集成Tomcat,Web请求的处理线程池是Tomcat的ThreadPoolExecutor。当我们开启虚拟线程后,Spring Boot会怎么改造Tomcat的线程池呢?

答案就在TomcatWebServerFactoryCustomizer里。当虚拟线程配置开启时,它会给Tomcat的ProtocolHandler设置一个虚拟线程执行器:

public class TomcatWebServerFactoryCustomizer implements WebServerFactoryCustomizer {

private final VirtualThreadsExecutorServiceConfigurer virtualThreadsConfigurer;

// 构造方法注入VirtualThreadsExecutorServiceConfigurer

@Override

public void customize(TomcatServletWebServerFactory factory) {

if (virtualThreadsConfigurer != null) {

factory.addProtocolHandlerCustomizers(protocolHandler -> {

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

protocolHandler.setExecutor(executor);

});

}

}

}

这里用Executors.newVirtualThreadPerTaskExecutor()创建了一个虚拟线程执行器,它底层用的是ForkJoinPool作为载体线程池,每个任务都会创建一个新的虚拟线程来执行。

当Tomcat接收到Web请求时,不再把任务提交给传统的平台线程池,而是交给虚拟线程执行器。虚拟线程执行器会为每个请求创建一个虚拟线程,然后把这个虚拟线程挂载到ForkJoinPool的工作线程(平台线程)上执行。

五、JVM层面的M:N调度:虚拟线程的挂载与卸载

Spring Boot的集成只是上层封装,真正的M:N调度逻辑在JVM层面。虚拟线程的挂载和卸载,主要依赖于ForkJoinPool和虚拟线程的调度器。

1. 虚拟线程的创建与挂载

当我们调用Executors.newVirtualThreadPerTaskExecutor().submit(task)时,底层会创建一个虚拟线程,并把它提交到ForkJoinPool的工作队列中。ForkJoinPool的工作线程(平台线程)会从队列中取出虚拟线程,然后调用VirtualThread.run()方法执行任务:

// 简化的虚拟线程执行逻辑

public class VirtualThread {

private Runnable task;

private CarrierThread carrier;

public void run() {

try {

task.run();

} finally {

// 执行完成后,将虚拟线程从载体线程上卸载

carrier.unmount(this);

}

}

public void mount(CarrierThread carrier) {

this.carrier = carrier;

// 保存虚拟线程的上下文

saveContext();

// 切换到虚拟线程的上下文执行

switchContext();

}

public void unmount() {

// 恢复载体线程的上下文

restoreContext();

this.carrier = null;

}

}

2. 阻塞时的卸载与恢复

当虚拟线程执行到IO操作(比如Thread.sleep()、数据库查询)时,JVM会触发虚拟线程的调度。以Thread.sleep()为例,它的底层实现会调用LockSupport.parkNanos(),这时候虚拟线程调度器会把虚拟线程从载体线程上卸载:

// 简化的LockSupport.parkNanos()逻辑

public static void parkNanos(long nanos) {

VirtualThread current = VirtualThread.current();

if (current != null) {

// 卸载虚拟线程

current.unmount();

// 将虚拟线程加入等待队列

scheduler.addToWaitQueue(current, nanos);

// 唤醒载体线程,让它去执行其他虚拟线程

CarrierThread.yield();

} else {

// 平台线程的park逻辑

UNSAFE.park(false, nanos);

}

}

当IO操作完成或者睡眠时间到了,调度器会把虚拟线程重新加入就绪队列,等待被挂载到空闲的载体线程上继续执行。

六、实战验证:用代码看M:N调度的效果

光说源码不够直观,咱们写个简单的Demo,看看虚拟线程的M:N调度到底是怎么运行的。

1. 测试代码

@SpringBootApplication

public class VirtualThreadDemoApplication {

public static void main(String[] args) {

SpringApplication.run(VirtualThreadDemoApplication.class, args);

}

@RestController

public class TestController {

@GetMapping("/test")

public String test() throws InterruptedException {

// 模拟IO操作

Thread.sleep(100);

return "当前线程:" + Thread.currentThread().getName() +

",是否为虚拟线程:" + Thread.currentThread().isVirtual();

}

}

}

2. 压测验证

用JMeter或者Postman并发发送1000个请求,然后查看日志。你会发现,处理请求的线程名都是virtual-xxx开头的虚拟线程,而载体线程的数量远小于1000(通常是CPU核心数)。这就证明了M:N调度确实在工作:少量的平台线程,处理了大量的虚拟线程任务。

七、写在最后:虚拟线程的优势与适用场景

通过源码分析我们能看到,虚拟线程的M:N调度,本质上是把线程调度的控制权从操作系统转移到了JVM,用用户态的轻量级切换,替代了内核态的重量级切换。这带来了几个核心优势:

极低的资源消耗:虚拟线程初始栈只有几KB,单机可轻松创建数百万个。

超高的并发能力:IO密集型场景下,吞吐量能提升数倍甚至数十倍。

简化并发编程:用同步的代码风格,实现异步的性能,不用再写复杂的回调或者Future。

不过也要注意,虚拟线程并非银弹。对于CPU密集型场景,虚拟线程的优势不明显,甚至不如传统线程池。但在Web接口、数据库操作、网络调用这些IO密集型场景里,它绝对是提升系统性能的利器。

希望这篇源码解析,能让你对Spring Boot虚拟线程的M:N调度有更深入的理解。如果你在使用过程中有什么问题,欢迎在评论区留言交流,咱们一起解锁虚拟线程的更多玩法!

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容