IWA
2025-11-22
点 赞
0
热 度
2
评 论
0

Java Virtual Threads(虚拟线程)教程:Spring Boot 实战(第 4 篇)


系列:《Java Virtual Threads(虚拟线程)教程:从零上手到实战优化》


第 4 篇:Spring Boot 与虚拟线程全面实战:从 Controller 到数据库的全链路优化

本篇是本系列的第一次“真正上手 Spring Boot 全链路”的教程。
我们会从 Controller → Service → Repository → DB 逐层改造,让整个 Web 应用真正跑在虚拟线程上。


目录

  1. Spring Boot 如何启用虚拟线程?

  2. Tomcat/Jetty/Undertow+虚拟线程的运行原理

  3. Web 层:Controller 使用虚拟线程

  4. Service 层:业务逻辑的并发优化

  5. Repository 层:数据库连接池与虚拟线程

  6. 虚拟线程适配 RestTemplate / WebClient

  7. 全链路示例:从请求到数据库的完整示例

  8. 压测对比:虚拟线程 vs 线程池

  9. 本篇总结


一、Spring Boot 如何启用虚拟线程?

Spring Boot 3+ 和 JDK 21 之后,已原生支持虚拟线程。

只需要一个配置类:

@Configuration
public class VirtualThreadConfig {

    @Bean
    public Executor taskExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

这样 Spring 的异步任务、@Async、Web 请求处理都可以使用虚拟线程。


二、Spring Boot 容器如何运行虚拟线程?

默认容器并不自动使用虚拟线程

  • Tomcat 还是用传统的 NIO + 线程池模型

  • Undertow、Jetty 同理

但 Spring MVC 的每个请求处理,可以放到虚拟线程执行。


Spring 官方推荐启用虚拟线程的方式:

方式 1:使用 ThreadPerTaskExecutor(全局)

@Bean
public Executor applicationTaskExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}

Spring 会将:

  • Controller 执行

  • @Async 方法

  • Web 请求处理链

全部放入虚拟线程中执行。


方式 2:仅为 Web 请求启用虚拟线程(官方推荐)

@Bean
public WebMvcConfigurer webMvcConfigurer() {
    return new WebMvcConfigurer() {
        @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            configurer.setTaskExecutor(Executors.newVirtualThreadPerTaskExecutor());
        }
    };
}

此方式仅改造 Web 层,其余不受影响。


方式 3:Tomcat 直接使用虚拟线程执行器(最彻底)

在 Spring Boot 3.2+ 可用:

@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreads() {
    return protocolHandler -> {
        protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
    };
}

作用:

  • Tomcat 的 request handling threads 全部换成虚拟线程

  • Web 层吞吐量直接提升数量级


三、Controller 使用虚拟线程:写法完全不变

你无需做任何改动,例如:

@RestController
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }
}

只要启用虚拟线程配置,这段代码就自动:

  • 每个请求一个虚拟线程

  • 阻塞数据库/HTTP 调用时自动挂起

性能大幅提升。


四、Service 层:虚拟线程让并发任务更轻松

例如你要调用 3 个远程服务:

传统写法(异步):

CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> userApi.getUser(id));
CompletableFuture<Order> order = CompletableFuture.supplyAsync(() -> orderApi.getOrder(id));
CompletableFuture<Point> point = CompletableFuture.supplyAsync(() -> pointApi.getPoints(id));

return new UserDetail(user.join(), order.join(), point.join());

虚拟线程写法(同步却更快):

public UserDetail getDetail(Long id) {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        Future<User> u = executor.submit(() -> userApi.getUser(id));
        Future<List<Order>> o = executor.submit(() -> orderApi.getOrders(id));
        Future<Point> p = executor.submit(() -> pointApi.getPoints(id));

        return new UserDetail(u.get(), o.get(), p.get());
    }
}

→ 同步写法,可读性极高
→ 并发能力强(甚至可以 1000 并发 RPC)


五、Repository 层:数据库连接池与虚拟线程

数据库是虚拟线程项目中最重要的瓶颈点

❗重要原则:

虚拟线程可以创建上万线程并发,但数据库连接池不能!

例如 HikariCP:

  • 默认 10 个连接

  • 就算你开 10000 个虚拟线程查询数据

  • 实际并行查询仍然只有 10 条


如何优化?

方式 1:适当增加连接池大小

建议:

  • 小型系统:20~50

  • 中型系统:50~100

  • 大型系统:100~300


方式 2:使用 R2DBC(真正异步 DB 驱动)

如果你想让虚拟线程彻底发挥 I/O 并发:

JDBC

R2DBC

同步阻塞

异步非阻塞

会造成 pinning(部分语句)

不会被阻塞

连接池需要调大

无线程池压力

Spring Boot 全面支持 R2DBC,让虚拟线程更高效。


六、REST 客户端:RestTemplate vs WebClient

RestTemplate(同步)+ 虚拟线程 = 强强组合

虚拟线程能完美支撑同步 HTTP 调用:

restTemplate.getForObject(url, String.class);

阻塞时自动挂起,不占用平台线程。


WebClient + 虚拟线程 = 不推荐

WebClient 是 Reactor(响应式),与虚拟线程不搭。

两者设计理念相反:

  • WebClient:事件驱动、非阻塞

  • Virtual Thread:同步阻塞、可挂起

推荐选择其一,而不是混用。

如果你启用了虚拟线程:

👉 99% 的项目使用 RestTemplate 更合适。


七、全链路示例(Web → Service → DB)

Controller

@GetMapping("/orders/{id}")
public OrderDetail getOrder(@PathVariable Long id) {
    return orderService.getOrderDetail(id);
}

Service

public OrderDetail getOrderDetail(Long id) {
    var order = orderRepository.findOrder(id);
    var items = orderRepository.findOrderItems(id);
    var coupon = couponApi.getCoupon(order.getCouponId());

    return new OrderDetail(order, items, coupon);
}

Repository(JDBC)

public Order findOrder(Long id) {
    return jdbcTemplate.queryForObject(
        "SELECT * FROM orders WHERE id = ?",
        new OrderMapper(),
        id
    );
}

以上所有代码:

  • 同步阻塞

  • 但是在虚拟线程里不会占用系统资源

  • 最后能实现 Netty/WebFlux 同级的吞吐量


八、压测对比:虚拟线程 VS 传统线程池

指标

传统线程池(Tomcat 200 线程)

虚拟线程(Tomcat+VirtualThread)

并发请求数

200

10000+

阻塞请求(DB/HTTP)

线程被卡死

自动挂起,不占资源

吞吐量

中等

很高

延迟

内存消耗

很低

线程调优

必须调

基本无需调

结果:
虚拟线程在 I/O 密集场景下的吞吐量比传统线程池高 3~20 倍。


九、本篇总结

本篇解决了“如何在 Spring Boot 使用虚拟线程”的实际问题,包括:

✔ 如何在 Spring Boot 中启用虚拟线程

✔ Controller → Service → Repository 全链路使用虚拟线程

✔ Tomcat 原生支持虚拟线程

✔ RestTemplate 与虚拟线程完美搭配

✔ 数据库连接池需要注意瓶颈

✔ 全链路示例可直接复制使用

✔ 压测结果说明虚拟线程的巨大价值

通过本篇,你已经可以:

在真实 Spring Boot 项目中落地 Virtual Threads,实现同步写法、异步性能、极高吞吐量的 Web 服务。


下一篇预告(第 5 篇)

《虚拟线程与数据库:JDBC、HikariCP、R2DBC 的最佳实践与踩坑总结》

内容将包括:

  • JDBC 会不会阻塞虚拟线程?

  • HikariCP 如何设置最合适的连接池大小?

  • 虚拟线程 + MySQL 的性能测试

  • 哪些 JDBC 驱动方法会触发 Pinning?

  • R2DBC + 虚拟线程能否达到极致性能?

  • 大型系统数据库访问架构推荐方案



用键盘敲击出的不只是字符,更是一段段生活的剪影、一个个心底的梦想。希望我的文字能像一束光,在您阅读的瞬间,照亮某个角落,带来一丝温暖与共鸣。

IWA

infp 调停者

具有版权性

请您在转载、复制时注明本文 作者、链接及内容来源信息。 若涉及转载第三方内容,还需一同注明。

具有时效性

文章目录

IWA的艺术编程,为您导航全站动态

35 文章数
9 分类数
10 评论数
32标签数
最近评论
IWA

IWA


👍

M丶Rock

M丶Rock


😂

M丶Rock

M丶Rock


感慨了

M丶Rock

M丶Rock


厉害了

M丶Rock

M丶Rock


6666666666666666666