IWA
2025-11-21
点 赞
0
热 度
4
评 论
0

Java Virtual Threads(虚拟线程)教程:同步写法获得异步性能(第 2 篇)


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


第 2 篇:使用 Virtual Threads 重构传统同步代码(HTTP / 数据库 / RPC 实战提升)

在第 1 篇中,我们理解了 Virtual Thread 的意义与特性,本篇将直接进入实战。

本篇目标非常明确:

把你现有必须用线程池的代码,用 Virtual Threads 重写成“更干净、更高性能、更容易维护”的版本。

并展示性能为何会显著提升。


一、传统线程池 vs 虚拟线程:本质区别

传统写法(通常是这样):

ExecutorService executor = Executors.newFixedThreadPool(200);

Future<String> result = executor.submit(() -> {
    return httpCall();
});

缺点:

  • 线程池大小有限(CPU & 内存限制)

  • 大量 I/O wait 时,线程被阻塞,整体并发能力差

  • 必须写 Future/Callback,可读性差

使用虚拟线程以后:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> httpCall());
}

优点立刻显现:

  • 你可以开 100000+ 线程 都没问题

  • “阻塞”=挂起,不占用平台线程

  • 代码保持同步风格,非常易读


二、实战 1:使用虚拟线程优化 HTTP 请求并发

这里以 Java 21 的 HttpClient 为例。


2.1 传统线程池版(需要 Future + 线程池)

ExecutorService executor = Executors.newFixedThreadPool(200);
HttpClient client = HttpClient.newHttpClient();

List<Future<String>> futures = new ArrayList<>();

for (int i = 0; i < 1000; i++) {
    futures.add(executor.submit(() -> {
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create("https://example.com"))
            .build();
        return client.send(req, HttpResponse.BodyHandlers.ofString()).body();
    }));
}

for (Future<String> f : futures) {
    System.out.println(f.get());
}

缺点:

  • 线程池 200 并发 → 只能同时处理 200 个请求

  • 请求慢时线程被阻塞 → 池子被占满

  • 后续任务排队 → 延迟高


2.2 使用虚拟线程重写(1000 个任务 = 1000 个线程)

HttpClient client = HttpClient.newHttpClient();

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = new ArrayList<>();

    for (int i = 0; i < 1000; i++) {
        futures.add(executor.submit(() -> {
            HttpRequest req = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .build();
            return client.send(req, HttpResponse.BodyHandlers.ofString()).body();
        }));
    }

    for (Future<String> f : futures) {
        System.out.println(f.get());
    }
}

为什么它更快?

因为在虚拟线程里:

  • HTTP 请求等待时间 ≠ 占用平台线程

  • 只在真正运行 Java 代码时占用 CPU

  • 等待网络 I/O → 虚拟线程自动挂起,不影响其他任务

同样是同步写法,吞吐量却接近异步 WebClient/Netty 的水平。


三、实战 2:数据库并发查询优化(重要)

数据库访问也是 I/O(网络、磁盘),非常适合虚拟线程。


3.1 传统:线程池版本

ExecutorService executor = Executors.newFixedThreadPool(50);

Future<List<User>> f = executor.submit(() -> {
    return jdbcTemplate.query("SELECT * FROM user", new UserMapper());
});

List<User> users = f.get();

缺点:

  • DB 查询慢,线程被阻塞

  • 线程池必须限制大小:太大线程切换成本高


3.2 虚拟线程版本(推荐写法)

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<List<User>> f = executor.submit(() -> {
        return jdbcTemplate.query("SELECT * FROM user", new UserMapper());
    });

    List<User> users = f.get();
}

收益:

  • 可同时开启成百上千 DB 查询,而不会吃掉 CPU

  • 无需调优线程池

  • 单个阻塞不会影响整体吞吐


3.3 批量数据库查询(典型业务场景)

场景:需要一次性查询 1000 个用户详情

传统线程池一般只能开几十个线程 → 查询慢

虚拟线程写法:

try (var exec = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<User>> futures = new ArrayList<>();

    for (Long userId : userIds) {
        futures.add(exec.submit(() -> {
            return jdbcTemplate.queryForObject(
                "SELECT * FROM user WHERE id = ?", new UserMapper(), userId
            );
        }));
    }

    List<User> results = new ArrayList<>();
    for (Future<User> f : futures) {
        results.add(f.get());
    }
}

这种写法看似普通,但吞吐量巨大提升:

传统线程池:50 并发
虚拟线程:1000+ 并发
毫无压力、性能极高


四、实战 3:RPC/微服务调用优化

假设你要同时请求多个微服务接口:

  • 获取用户详情

  • 获取用户订单

  • 获取用户积分

  • 获取用户等级


4.1 传统写法(CompletableFuture 版本)

CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(id));
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.getOrders(id));
CompletableFuture<Point> pointFuture = CompletableFuture.supplyAsync(() -> pointService.getPoints(id));

User user = userFuture.join();
List<Order> orders = orderFuture.join();
Point points = pointFuture.join();

缺点:

  • 写法复杂,可读性差

  • 调优线程池成本高


4.2 虚拟线程版(同步写法反而更快)

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    Future<User> userFuture = executor.submit(() -> userService.getUser(id));
    Future<List<Order>> orderFuture = executor.submit(() -> orderService.getOrders(id));
    Future<Point> pointFuture = executor.submit(() -> pointService.getPoints(id));

    User user = userFuture.get();
    List<Order> orders = orderFuture.get();
    Point points = pointFuture.get();
}

更爽的写法(不使用 Future):

User user = Thread.ofVirtual().unstarted(() -> userService.getUser(id)).start().join();

五、使用虚拟线程最佳写法(官方推荐)

Java 官方推荐这样写:

每个任务一个虚拟线程 Executor

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(task);
}

优势:

  • 自动为每个 task 创建独立 Virtual Thread

  • 不用自己管理生命周期

  • 线程泄漏风险极低


六、性能分析(为什么吞吐量暴涨)

虚拟线程最大的价值:

让同步阻塞变成“无成本等待”,让 I/O 密集型服务吞吐量几乎线性提升。

原因:

  • 传统线程:阻塞 = 占用 OS Thread

  • 虚拟线程:阻塞 = 让出 OS Thread(挂起),其他任务继续执行

因此:

  • DB、Redis、HTTP、RPC 都是 I/O → 全部大幅提升性能

  • 用同步写法实现异步运行 → 开发效率提高


七、本篇总结

本篇解决了 Virtual Threads 最关键的价值:

✔ 同步写法(阻塞风格)能够获得异步性能

✔ 如何用虚拟线程重写 HTTP / DB / RPC 并发请求

✔ 代码极度简化,可读性提高

✔ 每个任务一个线程,性能更好,设计更简单

✔ 不需要线程池调优


下一篇预告(第 3 篇)

《虚拟线程调度机制深度解析:平台线程、纤程、栈帧、挂起与恢复》

内容包括:

  • 虚拟线程到底如何挂起?

  • Platform Thread 与 Virtual Thread 是怎么协作的?

  • Virtual Thread 的“栈”是怎么管理的?

  • 为什么同步 I/O 能跑出异步性能?

  • Pinning(线程钉住)为何会影响性能?(非常重要)



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

IWA

infp 调停者

具有版权性

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

具有时效性

文章目录

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

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

IWA


👍

M丶Rock

M丶Rock


😂

M丶Rock

M丶Rock


感慨了

M丶Rock

M丶Rock


厉害了

M丶Rock

M丶Rock


6666666666666666666