找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2638

积分

0

好友

376

主题
发表于 15 小时前 | 查看: 0| 回复: 0

最近我在测试 SpringBoot 4.0 正式版时,想看看虚拟线程在不同 Web 容器下的表现。Undertow 之前已经验证过不太理想,所以这次主要对比剩下两位“选手”:Tomcat 和 Jetty。

我原本的想法很简单:虚拟线程是 Java 层面的特性,容器只是提供运行环境,两者的性能表现应该差不多吧?

但实际的压测结果让我吃了一惊,它们的差距竟然达到了惊人的15倍。

Tomcat与Jetty开启虚拟线程的性能对比柱状图

先说结论

如果你正在或计划使用 SpringBoot 4.0 并开启虚拟线程,那么选择 Tomcat,不用犹豫。
Jetty 在虚拟线程模式下的表现,几乎和没开启时一样。

测试环境说明

为了让测试结果具备参考价值,先将测试环境交代清楚:

  • SpringBoot: 4.0.1
  • JDK: 25
  • 测试机器: MacBook Pro M4,32GB 内存
  • Docker 镜像内存限制: 1024MB
  • 压测工具: wrk
  • 压测时长: 30 秒

测试接口设计得很简单,模拟了一个支付场景。一个 /pay 接口,内部通过 Thread.sleep(1000) 来模拟调用支付网关等外部 IO 阻塞操作。

PayService 实现如下:

@Service
public class PayService {

    private static final String[] CHANNELS =
        new String[]{"ALIPAY", "WECHAT", "UNION", "VISA", "MASTER"};

    public String doPay(String orderId) {
        try {
            String channel = CHANNELS[ThreadLocalRandom.current()
                .nextInt(CHANNELS.length)];
            Thread.sleep(1000);  // 模拟调用支付网关
            return "Order %s paid via %s".formatted(orderId, channel);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

对应的 Controller:

@RestController
@RequestMapping("/pay")
public class PayController {

    private static final Logger log =
        LoggerFactory.getLogger(PayController.class);
    private static final AtomicInteger counter = new AtomicInteger(1);

    private final PayService payService;

    public PayController(PayService payService) {
        this.payService = payService;
    }

    @GetMapping
    public String pay(
            @RequestParam(required = false, defaultValue = "PIG2026")
            String orderId) {
        int id = counter.getAndIncrement();
        log.info("Request {} processed by {}", id, Thread.currentThread());
        String result = payService.doPay(orderId);
        log.info("Request {} resumed by {}", id, Thread.currentThread());
        return result;
    }
}

四组对照镜像

为了进行严谨的对比,我构建了四个 Docker 镜像,形成完整的对照组:

镜像名 容器 虚拟线程
tomcat Tomcat 关闭
tomcat-vt Tomcat 开启
jetty Jetty 关闭
jetty-vt Jetty 开启

开启虚拟线程的配置极其简单,只需在 application.properties 中添加一行:

spring.threads.virtual.enabled=true

使用 GraalVM Native Image 构建

本次测试全部采用 GraalVM Native Image 构建应用。SpringBoot 4.0 对 Native 支持已经相当成熟。构建命令核心是添加 -Pnative 参数。

构建 Tomcat Native 镜像

# 不开虚拟线程
./mvnw clean -DskipTests spring-boot:build-image -Pnative \
  -Dspring-boot.build-image.imageName=pig-pay-tomcat:4.0.0-25-native

# 开启虚拟线程
echo "spring.threads.virtual.enabled=true" >> src/main/resources/application.properties
./mvnw clean -DskipTests spring-boot:build-image -Pnative \
  -Dspring-boot.build-image.imageName=pig-pay-tomcat:4.0.0-25-native-vt

构建 Jetty Native 镜像

# 不开虚拟线程(注意多了 -Pjetty)
./mvnw clean -DskipTests spring-boot:build-image -Pjetty,native \
  -Dspring-boot.build-image.imageName=pig-pay-jetty:4.0.0-25-native

# 开启虚拟线程
echo "spring.threads.virtual.enabled=true" >> src/main/resources/application.properties
./mvnw clean -DskipTests spring-boot:build-image -Pjetty,native \
  -Dspring-boot.build-image.imageName=pig-pay-jetty:4.0.0-25-native-vt

构建完成后得到四个镜像。Native Image 的优势是启动快、内存占用小,缺点是构建过程较慢。

压测结果:数据说话

不开虚拟线程的基线对比

首先,在未开启虚拟线程的传统模式下,Tomcat 和 Jetty 的表现如何?

并发数 Tomcat QPS Jetty QPS 差距
100 95.96 94.19 基本持平
500 192.76 187.08 Tomcat 略优
1000 192.92 187.06 Tomcat 略优
3000 179.49 171.11 Tomcat 略优
5000 114.23 98.66 Tomcat 略优

两者差距很小,性能瓶颈都被限制在200个平台线程左右,这符合我们的预期。

开启虚拟线程后的巨大分野

重点来了,开启虚拟线程后的对比数据让我一度以为测试出了错:

并发数 Tomcat-VT QPS Jetty-VT QPS 差距
100 96.45 95.53 基本持平
500 477.99 191.90 Tomcat 2.5 倍
1000 947.68 191.96 Tomcat 5 倍
3000 2699.67 178.13 Tomcat 15 倍
5000 616.43 112.09 Tomcat 5.5 倍

这个数据非常直观:Tomcat 开启虚拟线程后,在3000并发下 QPS 从179飙升到近2700,提升约15倍。而 Jetty 开启虚拟线程后,性能曲线几乎原地踏步,和没开一样。

Tomcat开启虚拟线程前后性能对比柱状图

Jetty 为何“纹丝不动”?

为什么会出现如此巨大的差异?经过一番排查,原因在于线程池的管理策略。
即使为 Jetty 开启了虚拟线程,它仍然受到配置 server.jetty.threads.max=200 的严格限制。虚拟线程的“廉价”特性——可以轻松创建数万个——在 Jetty 传统的线程池管理模型下完全无法发挥。

反观 Tomcat,在检测到虚拟线程启用后,它变得更加“豁达”,直接跳过了 server.tomcat.threads.max=200 这类对平台线程的数量限制,让虚拟线程能够根据实际需求自由创建和调度,从而在 高并发场景 下彻底释放了性能潜力。

启动时间对比

既然用了 Native Image,也顺便看一下启动速度:

镜像 启动时间
tomcat 0.643s
tomcat-vt 0.658s
jetty 0.806s
jetty-vt 0.710s

四者的启动时间都在1秒以内,这个维度上差别不大,Native Image 的快速启动优势都很明显。

如何从 Jetty 切换回 Tomcat

如果你现有的项目使用的是 Jetty,想切换回 Tomcat 以获得虚拟线程的全部优势,操作非常简单。

原来的 Jetty 配置(需要在依赖中排除 Tomcat 并引入 Jetty):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

切换到 Tomcat:你只需要恢复使用 Spring Boot Web 的默认依赖即可,无需任何额外排除操作。

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

然后在配置文件中开启虚拟线程:

spring.threads.virtual.enabled=true

这样就完成了切换。

总结

回顾一下,去年技术社区可能还在讨论用 Undertow 替代 Tomcat 以获得更好性能,结果 Undertow 在 SpringBoot 4.0 的虚拟线程支持上掉了队。这次测试再次表明,在技术选型上,有时“稳健且持续演进的主流方案”比“看似激进的替代方案”更值得信赖。

Tomcat 或许不是最时髦的那个,但它始终紧跟标准,从 Servlet 1.0 到 6.1,从平台线程到虚拟线程,每一次技术演进它都没有缺席。在虚拟线程这个新时代,如果你追求极致的性能表现,“汤姆猫”依然是那个可靠的选择。欢迎在 云栈社区 分享你在使用虚拟线程或进行技术选型时的经验和见解。




上一篇:SolidWorks 面板设计教程:从草图到PCB开孔的完整流程
下一篇:技术选型:PostgreSQL与MySQL的核心差异及适用场景剖析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-28 18:10 , Processed in 0.332645 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表