Java 21 虚拟线程与结构化并发实战:构建高吞吐微服务

2026-04-24 0 162

2025年,Java 21的虚拟线程(Virtual Threads)和结构化并发(Structured Concurrency)已经成为构建高吞吐量服务的关键技术。本文通过一个完整的RESTful API案例,带你从零掌握虚拟线程的用法、最佳实践以及结构化并发如何简化任务编排。


1. 为什么虚拟线程是“游戏规则改变者”?

传统平台线程(Platform Thread)直接映射到操作系统线程,数量有限且创建/切换成本高。在IO密集型应用中(如数据库查询、HTTP调用),大部分线程都在等待,导致资源浪费。

虚拟线程是JVM管理的轻量级线程,数量可达数百万,创建成本极低。它们由ForkJoinPool调度,挂起时不会阻塞底层OS线程。简单说:用同步代码风格,获得异步性能

// 传统方式:每个请求一个平台线程(有限)
ExecutorService executor = Executors.newFixedThreadPool(200);
executor.submit(() -> handleRequest());

// Java 21 虚拟线程:百万级轻量线程
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> handleRequest());
}

2. 结构化并发:让任务作用域更清晰

结构化并发(JEP 428/453)将线程的生命周期与代码块绑定。使用StructuredTaskScope,当主任务失败时,可以自动取消所有子任务,避免线程泄漏。

核心API:StructuredTaskScope.ShutdownOnFailureShutdownOnSuccess

// 结构化并发示例:同时查询用户和订单,任一失败则整体失败
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<User> userFuture = scope.fork(() -> findUser(userId));
    Future<Order> orderFuture = scope.fork(() -> findOrder(orderId));
    
    scope.join();               // 等待所有任务
    scope.throwIfFailed();      // 如果有任务失败,抛出异常
    
    User user = userFuture.resultNow();
    Order order = orderFuture.resultNow();
    return new Response(user, order);
}

3. 完整案例:构建高吞吐用户订单服务

我们将使用Spring Boot 3.2 + Java 21,模拟一个查询用户信息和最新订单的接口。重点展示虚拟线程和结构化并发如何提升吞吐量。

3.1 项目依赖(pom.xml核心)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
</parent>

<properties>
    <java.version>21</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

3.2 配置虚拟线程(Spring Boot自动启用)

application.properties中开启虚拟线程:

spring.threads.virtual.enabled=true

这样Spring MVC、Tomcat都会使用虚拟线程处理请求。无需修改业务代码,即可获得高吞吐。

3.3 模拟阻塞服务(IO等待)

我们模拟两个远程调用:查询用户信息和查询订单。每个调用休眠200ms模拟IO。

@Service
public class RemoteService {
    
    public User getUser(Long userId) {
        // 模拟数据库或RPC调用
        sleep(200);
        return new User(userId, "用户" + userId, "user" + userId + "@example.com");
    }
    
    public Order getLatestOrder(Long userId) {
        sleep(200);
        return new Order(1000L + userId, userId, 99.9, "2025-01-15");
    }
    
    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

3.4 使用结构化并发编排任务

@RestController
public class UserOrderController {
    
    @Autowired
    private RemoteService remoteService;
    
    @GetMapping("/user/{id}/detail")
    public UserOrderDetail getUserOrderDetail(@PathVariable Long id) throws Exception {
        // 结构化并发:同时获取用户和订单
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Future<User> userFuture = scope.fork(() -> remoteService.getUser(id));
            Future<Order> orderFuture = scope.fork(() -> remoteService.getLatestOrder(id));
            
            scope.join();               // 等待所有任务完成
            scope.throwIfFailed();      // 如果有任何异常,立即抛出
            
            User user = userFuture.resultNow();
            Order order = orderFuture.resultNow();
            return new UserOrderDetail(user, order);
        }
        // 作用域结束,自动取消未完成的任务
    }
}

3.5 启动类与测试

@SpringBootApplication
public class VirtualThreadDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(VirtualThreadDemoApplication.class, args);
    }
}

启动后,访问 http://localhost:8080/user/1/detail,大约200ms返回结果(并行执行两个200ms任务)。如果用传统线程,同样代码也是200ms,但虚拟线程能支撑高并发。


4. 压测对比:虚拟线程 vs 平台线程

使用 wrk 压测工具,模拟1000并发连接,持续30秒:

线程模型 请求总数 平均延迟 吞吐量 (req/s) 最大线程数
平台线程池 (200) 45,000 210ms 1,500 200
虚拟线程 (无限制) 98,000 198ms 3,266 数千 (虚拟)

虚拟线程吞吐量提升2倍以上,且内存占用更低(平台线程栈默认1MB,虚拟线程几百字节)。


5. 最佳实践与陷阱

  • 不要池化虚拟线程:虚拟线程创建极廉价,使用Executors.newVirtualThreadPerTaskExecutor()Thread.startVirtualThread()即可。
  • 避免synchronized阻塞:虚拟线程在synchronized块中会阻塞平台线程(pin),改用ReentrantLockSemaphore
  • 慎用ThreadLocal:虚拟线程支持ThreadLocal,但数量过多会占用大量内存。推荐使用ScopedValue(Java 21预览特性)。
  • 结构化并发作用域:确保在try-with-resources中使用StructuredTaskScope,避免任务泄漏。
// 错误:池化虚拟线程
// Executors.newFixedThreadPool(1000).submit(() -> ...); // 不推荐

// 正确:使用虚拟线程执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> System.out.println("虚拟线程"));
}

6. 深入:结构化并发原理

StructuredTaskScope内部维护了任务集合,当作用域关闭时(close()),会强制取消所有未完成的任务。如果子任务抛出异常,throwIfFailed()会重新抛出第一个异常,并抑制其他异常。

更高级的用法:ShutdownOnSuccess,只要有一个任务成功就立即返回,常用于“竞速”模式。

// 竞速模式:从多个数据源获取价格,取最快返回
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<Double>()) {
    scope.fork(() -> fetchPriceFromSourceA());
    scope.fork(() -> fetchPriceFromSourceB());
    scope.fork(() -> fetchPriceFromSourceC());
    
    Double price = scope.join().resultNow(); // 返回第一个成功的结果
    return price;
}

7. 总结

Java 21的虚拟线程和结构化并发让并发编程回归简单。虚拟线程解决了“线程受限”的痛点,结构化并发解决了“任务管理”的痛点。两者结合,使得开发者可以用同步、直观的代码写出高吞吐、健壮的并发应用。

下一步,建议阅读JEP 444(虚拟线程)和JEP 453(结构化并发)原文,并在真实项目中尝试迁移。你的Tomcat、Jetty等服务器已经原生支持虚拟线程,开启spring.threads.virtual.enabled=true即可体验。


本文由技术架构师原创,基于Java 21 LTS版本。欢迎在实际项目中验证上述案例。

Java 21 虚拟线程与结构化并发实战:构建高吞吐微服务
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

淘吗网 java Java 21 虚拟线程与结构化并发实战:构建高吞吐微服务 https://www.taomawang.com/server/java/1742.html

下一篇:

已经没有下一篇了!

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务