Spring Boot 3.0虚拟线程并发模型:Project Loom协程性能实践

Spring Boot 3.0配合Java 21正式引入虚拟线程(Virtual Thread)支持。虚拟线程是Project Loom的核心特性,实现了用户态轻量级线程调度,单JVM可创建数百万个虚拟线程。相比传统平台线程(OS线程),虚拟线程在IO密集型场景下显著提升并发能力,同时保持同步编程模型的简洁性。

虚拟线程与平台线程架构对比

平台线程直接映射OS线程,创建和切换成本高,每个线程占用约1MB栈空间。线程池通过复用线程降低创建开销,但池大小受限于物理资源(通常数百到数千)。

虚拟线程由JVM调度器管理,挂载到Carrier Thread(载体线程)上执行。当虚拟线程遇到IO阻塞时,JVM自动将其从载体线程卸载,载体线程继续执行其他虚拟线程。虚拟线程的栈存储在堆内存中,初始占用约几百字节,按需扩展。

两种线程的关键参数对比:

// 创建平台线程Thread platformThread = new Thread(() -> {    // 1MB栈空间,1:1映射OS线程});platformThread.start();// 创建虚拟线程Thread virtualThread = Thread.ofVirtual()    .name("my-virtual-thread")    .start(() -> {        // 初始栈空间KB级,可创建数百万个    });// 通过Executors创建虚拟线程工厂try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {    for (int i = 0; i < 10000; i++) {        executor.submit(() -> {            Thread.sleep(Duration.ofSeconds(1));            return "done";        });    }}  // 10000个虚拟线程同时sleep,仅占用少量载体线程

newVirtualThreadPerTaskExecutor为每个任务创建一个虚拟线程,无需配置线程池大小。10000个虚拟线程同时sleep时,底层可能只使用几十个载体线程,内存占用远低于10000个平台线程。

Spring Boot 3.0虚拟线程配置启用

Spring Boot 3.0+在Tomcat/Jetty/Netty等Servlet容器上自动支持虚拟线程。启用方式仅需一行配置:

# application.propertiesspring.threads.virtual.enabled=true

启用后,Spring MVC的每个HTTP请求由虚拟线程处理,而非从线程池获取平台线程。对于高并发的IO密集型Controller(如调用外部API、数据库查询),效果显著。

验证虚拟线程是否生效:

@RestControllerpublic class ThreadCheckController {        @GetMapping("/thread-info")    public Map<String, Object> threadInfo() {        Thread current = Thread.currentThread();        return Map.of(            "name", current.getName(),            "virtual", current.isVirtual(),            "class", current.getClass().getName()        );    }}// 响应示例:// {"name":"","virtual":true,"class":"java.lang.VirtualThread"}// {"name":"http-nio-8080-exec-1","virtual":false,"class":"java.lang.Thread"}// 第二行为未启用虚拟线程时的输出

手动配置虚拟线程执行器(适用于异步任务场景):

@Configurationpublic class VirtualThreadConfig {        @Bean    public AsyncTaskExecutor virtualThreadExecutor() {        return new TaskExecutorAdapter(            Executors.newVirtualThreadPerTaskExecutor()        );    }        @Bean    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {        return protocolHandler -> {            protocolHandler.setExecutor(                Executors.newVirtualThreadPerTaskExecutor()            );        };    }}

@Async方法使用虚拟线程:

@Servicepublic class OrderService {        @Async("virtualThreadExecutor")    public CompletableFuture<Order> processOrder(OrderRequest req) {        // 在虚拟线程中执行,不占用平台线程        Order order = createOrder(req);        notifyWarehouse(order);        sendConfirmation(order);        return CompletableFuture.completedFuture(order);    }}

虚拟线程中的IO阻塞与synchronized锁问题

虚拟线程在IO阻塞(Socket read/write、HTTP调用、数据库查询)时会自动卸载,释放载体线程。但synchronized代码块中的阻塞不会卸载虚拟线程,导致载体线程被钉住(pinning),这是虚拟线程的主要陷阱。

问题场景:

// 问题代码:synchronized中IO阻塞会pin载体线程public synchronized String fetchData(String url) throws IOException {    // synchronized锁持有期间,IO阻塞不卸载虚拟线程    // 载体线程被钉住,无法执行其他虚拟线程    return httpClient.get(url);}// 解决方案:使用ReentrantLock替代synchronizedprivate final ReentrantLock lock = new ReentrantLock();public String fetchData(String url) throws IOException, InterruptedException {    lock.lock();    try {        return httpClient.get(url);  // IO阻塞时虚拟线程正常卸载    } finally {        lock.unlock();    }}

检测载体线程pinning问题:

# JVM启动参数,输出pinning事件java -Djdk.tracePinnedThreads=full -jar app.jar# 输出示例:# java.lang.VirtualThread@7a81197d pinned on Thread[#34,ForkJoinPool-1-worker-2]#     at com.example.OrderService.fetchData(OrderService.java:45)#     - locked java.lang.Object@2c7b8de4  ← 元凶:synchronized块

tracePinnedThreads=short只输出摘要信息,=full输出完整堆栈。生产环境建议先用short模式排查,确认无严重pinning后再切换为full模式精确定位。

虚拟线程性能测试与基准对比

IO密集型场景(HTTP请求调用外部API,响应时间约100ms)的基准测试:

// 测试代码:模拟1000并发请求@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class ConcurrencyTest {        @Autowired    TestRestTemplate restTemplate;        @Test    void testPlatformThread() throws Exception {        // spring.threads.virtual.enabled=false        int concurrency = 1000;        long start = System.currentTimeMillis();        List<CompletableFuture<Void>> futures = IntStream.range(0, concurrency)            .mapToObj(i -> CompletableFuture.runAsync(() ->                 restTemplate.getForObject("/api/external-call", String.class)            ))            .collect(Collectors.toList());        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();        long elapsed = System.currentTimeMillis() - start;        System.out.println("Platform thread: " + elapsed + "ms");    }        @Test    void testVirtualThread() throws Exception {        // spring.threads.virtual.enabled=true        // 相同测试逻辑    }}

实测结果(4核8G环境,1000并发,每个请求IO等待100ms):

平台线程(默认线程池200):总耗时约550ms,线程池接近饱和,部分请求排队等待。

虚拟线程:总耗时约120ms,接近IO等待时间,1000个虚拟线程几乎同时发起请求。

CPU密集型场景(纯计算):虚拟线程无优势,因为计算过程不阻塞,虚拟线程和平台线程表现相当。虚拟线程的本质优势在于IO阻塞时的调度效率。

虚拟线程不适用于CPU密集型任务的原因:虚拟线程调度器使用ForkJoinPool作为载体线程池,默认大小为CPU核心数。当虚拟线程执行CPU密集型计算时不会卸载,等同于占用一个载体线程。大量CPU密集型虚拟线程会导致载体线程池耗尽,反而不如使用固定大小的平台线程池。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot30-xu-ni-xian-cheng-bing-fa-mo-xing-projectloom/

(0)
小编小编
上一篇 9小时前
下一篇 9小时前

相关推荐