Java 21虚拟线程实战:用协程式编程彻底解决高并发I/O瓶颈

2026-06-27 0 997

传统 Java 开发里,处理大量并发 I/O 操作时,线程池的配置总是让人头疼:线程数设少了吞吐量上不去,设多了内存和上下文切换开销又难以承受。Java 21 正式引入的虚拟线程(Virtual Threads)让局面变得完全不同。它把开发者从“线程是一种昂贵资源”的思维定式中解放出来,让你可以像创建字符串一样创建线程。这篇文章不讲虚的,直接带你用代码上手虚拟线程,并且对比它在真实并发场景下的表现。

一、虚拟线程解决了什么问题

先回顾一下传统平台线程的工作方式。一个典型的 Web 服务器会为每个请求分配一个线程,这个线程在执行数据库查询或调用外部 API 时会被阻塞,操作系统会暂停它、保留它的栈、切换到另一个线程。如果同时有几千个这样的阻塞操作,操作系统就需要在几千个线程之间频繁切换,CPU 大量时间耗费在上下文切换和内核调度上,真正干活的时间反而减少了。

异步编程(比如使用 CompletableFuture 或响应式框架)可以缓解这个问题,但它牺牲了代码的可读性:原本一个方法从上到下写完的逻辑,被迫拆成若干个回调,调试和排查异常变得痛苦。

虚拟线程的思路截然不同。它仍然让你使用熟悉的同步、阻塞式代码风格,但阻塞一个虚拟线程时,底层只挂起一个极轻量级的对象,操作系统的真实线程(载体线程)立即去执行另一个虚拟线程。当阻塞操作完成,虚拟线程被恢复,继续往下执行。整个过程不需要操作系统的线程调度参与,完全由 JVM 在用户态完成。结果是:你可以安全地创建数十万甚至上百万个虚拟线程,而不用担心内存耗尽或调度风暴。

二、创建并运行第一个虚拟线程

确保你的环境是 Java 21 或更高版本。虚拟线程的 API 主要在java.lang.Thread类和java.util.concurrent.Executors类中。

最简单的创建方式跟普通线程几乎一样,只是多了一个工厂方法:

// 方式1:直接启动一个虚拟线程
Thread.startVirtualThread(() -> {
    System.out.println("Hello from a virtual thread: " + Thread.currentThread());
});

// 让主线程等待一会儿,因为虚拟线程是守护线程
Thread.sleep(1000);

运行后你会看到类似这样的输出,注意线程名称里带有“VirtualThread”:

Hello from a virtual thread: VirtualThread[#21]/runnable@ForkJoinPool-1-worker-1

另一种更常用的方式是通过Thread.ofVirtual() builder:

Thread vThread = Thread.ofVirtual()
        .name("my-virtual-thread")
        .unstarted(() -> {
            // 你的任务
        });
vThread.start();
vThread.join();

还可以用虚拟线程专用的执行器:

import java.util.concurrent.Executors;

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> {
        System.out.println("Task running in virtual thread");
    });
}

newVirtualThreadPerTaskExecutor()返回一个为每一个任务都创建全新虚拟线程的执行器,用完记得关闭。这种执行器特别适合批处理任务,你不需要操心池子里有多少线程,提交十万个任务就会创建十万个虚拟线程,JVM 内部会用少量载体线程来承载它们。

三、实战对比:模拟高并发HTTP请求处理

光说理论没说服力,我们写一个小型模拟程序:假设一个服务需要处理大量并发请求,每个请求内部要调用一个“外部服务”(模拟网络 I/O),阻塞等待 100 毫秒。我们来比较传统固定线程池和虚拟线程的吞吐量差异。

传统平台线程池版本

import java.util.concurrent.*;

public class TraditionalServer {
    public static void main(String[] args) throws Exception {
        int requestCount = 1000;
        ExecutorService pool = Executors.newFixedThreadPool(200);
        long start = System.currentTimeMillis();

        CountDownLatch latch = new CountDownLatch(requestCount);
        for (int i = 0; i  {
                try {
                    // 模拟等待外部服务响应 100ms
                    simulateIoBlock();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        long time = System.currentTimeMillis() - start;
        System.out.println("平台线程池处理 " + requestCount + " 个请求耗时: " + time + " ms");
        pool.shutdown();
    }

    static void simulateIoBlock() {
        try { Thread.sleep(100); } catch (InterruptedException e) { }
    }
}

典型输出:平台线程池处理 1000 个请求耗时: 518 ms (结果因硬件而异)

线程池只有 200 个线程,1000 个请求需要排队执行,每一组 200 个请求并行,耗时大约等于 5 个 100ms 周期,加上调度开销,500 多毫秒符合预期。

虚拟线程版本

import java.util.concurrent.*;

public class VirtualServer {
    public static void main(String[] args) throws Exception {
        int requestCount = 1000;
        long start = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(requestCount);

        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i  {
                    try {
                        simulateIoBlock();
                    } finally {
                        latch.countDown();
                    }
                });
            }
        }

        latch.await();
        long time = System.currentTimeMillis() - start;
        System.out.println("虚拟线程处理 " + requestCount + " 个请求耗时: " + time + " ms");
    }

    static void simulateIoBlock() {
        try { Thread.sleep(100); } catch (InterruptedException e) { }
    }
}

运行输出会让人眼前一亮:虚拟线程处理 1000 个请求耗时: 127 ms

1000 个并发虚拟线程几乎同时启动,每个阻塞 100 毫秒后几乎同时恢复,总耗时只略大于 100 毫秒。这就是虚拟线程的威力:在 I/O 密集型场景中,吞吐量不再受限于操作系统能支撑的线程数量,而是受限于 I/O 本身的延迟和 CPU 处理能力。

你可以把requestCount改成 10000 甚至 50000,虚拟线程版本依然能在 100 多毫秒内完成,而平台线程池版本要么 OOM,要么耗时长得无法接受。

四、虚拟线程在真实场景中的用法

上面的例子毕竟只是模拟,接入真实业务我们可以用 Spring Boot 3.2+ (内置了虚拟线程支持)来体验。如果你还在用 Servlet 容器,Tomcat 10.1+ 和 Jetty 12 都支持将请求处理交给虚拟线程执行。

Spring Boot 启用虚拟线程

application.properties中加入一行即可:

spring.threads.virtual.enabled=true

这样,Spring Boot 的 Tomcat 请求处理线程、定时任务线程,甚至@Async注解的方法都会运行在虚拟线程上。你的控制器代码不用做任何改动,依然是熟悉的同步风格:

@RestController
public class OrderController {

    @GetMapping("/order/{id}")
    public Order getOrder(@PathVariable String id) {
        // 这里可以放心使用阻塞的数据库查询、HTTP调用
        var order = orderRepository.findById(id); // 假设这是一个阻塞调用
        var delivery = restTemplate.getForObject("http://delivery/status/" + id, String.class);
        return order.withDeliveryStatus(delivery);
    }
}

当大量并发请求进来时,每个请求都会在独立的虚拟线程中处理,即使orderRepositoryrestTemplate是阻塞调用,底层的载体线程也不会被白白占用,整体吞吐量会显著提升。

数据库连接池与虚拟线程的注意事项

使用虚拟线程时,数据库连接池通常会成为一个新的瓶颈。因为每个虚拟线程都需要从池里获取连接,如果池大小只有 20,那么同时只有 20 个虚拟线程能真正执行数据库操作,其余都在等连接。这时需要适当调大连接池大小,或者考虑使用支持异步的非阻塞数据库驱动(如 R2DBC),但 R2DBC 本身就已经是异步编程了,与你使用虚拟线程简化代码的初衷有些相悖。一种折中方案是为不同的外部资源合理分配连接池上限,让虚拟线程的主要威力发挥在外部 HTTP 调用、消息队列交互等慢 I/O 上。

五、虚拟线程的调度与可观测性

虚拟线程的调度由 JVM 内部的 ForkJoinPool 负责,默认使用一个公共的载体线程池。你可以通过系统属性调整载体线程的数量,但通常不需要,因为调度逻辑已经做了大量优化。

如果你需要对虚拟线程进行监控,JDK 自带的 jcmdJava Flight Recorder 都已支持虚拟线程事件。使用 JFR 录制时勾选 jdk.VirtualThreadStartjdk.VirtualThreadEnd 等事件,就能看到每个虚拟线程的创建、挂起和恢复时间,对于排查性能瓶颈非常有用。

简单演示一下代码中查看当前虚拟线程数量:

ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
// 开启线程统计(默认可能关闭)
if (mxBean instanceof com.sun.management.ThreadMXBean sunMxBean) {
    sunMxBean.setThreadAllocatedMemoryEnabled(true);
    sunMxBean.setThreadCpuTimeEnabled(true);
}
// 遍历所有线程,通过线程名称判断虚拟线程
for (long id : mxBean.getAllThreadIds()) {
    ThreadInfo info = mxBean.getThreadInfo(id);
    if (info != null && info.getThreadName().startsWith("VirtualThread")) {
        System.out.println(info.getThreadName() + " state: " + info.getThreadState());
    }
}

这种工具性的检查在开发调试阶段很有帮助,可以及时发现是否有虚拟线程泄漏(比如忘记 join 或关闭执行器)。

六、当虚拟线程不适用时

虚拟线程虽然强大,但并不是所有场景的万能药。当你的任务主要是 CPU 密集型计算(如图像处理、加密解密),且极少阻塞时,使用虚拟线程不会有任何好处,反而可能因为频繁切换载体线程带来额外开销。此时普通平台线程或 ForkJoinPool 的并行流更合适。

另外,如果你在虚拟线程内使用synchronized块或 JNI 调用长时间持有监视器锁,可能会导致载体线程被固定(pinning),暂时降低调度的灵活性。Java 团队正在逐步优化这种情况,但在完全消除之前,极度频繁且长时间持有的锁应尽量替换为java.util.concurrent.locks.Lock

七、迁移策略小结

对于现有的 Java 项目,引入虚拟线程不必一蹴而就。你可以遵循下面几步渐进式地享受它带来的好处:

  1. 将 JDK 升级到 21 或更高版本。
  2. 在需要高并发 I/O 的模块中,用 Executors.newVirtualThreadPerTaskExecutor() 替换原先的 newFixedThreadPool(n),并调整连接池等资源上限。
  3. Web 应用如果使用 Spring Boot 3.2+,直接在配置中开启虚拟线程支持。
  4. 监控 GC 和内存使用情况,因为大量虚拟线程对象会占用堆内存,如果并发量极高,可能需要按需调整堆大小。
  5. 逐步从异步编码风格回归到同步阻塞代码,只要底层 I/O 操作能被虚拟线程有效调度,代码的可读性将得到巨大提升。

虚拟线程标志着 Java 并发编程从“线程池精细化控制”进入“直接基于业务逻辑建模”的新阶段。它让你不再为“应该开多少个线程”而纠结,把注意力放回业务本身。这是一个值得立即投入实战的特性,尤其是在微服务调用链、网关代理、消息消费等典型的 I/O 密集型系统中。

Java 21虚拟线程实战:用协程式编程彻底解决高并发I/O瓶颈
收藏 (0) 打赏

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

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

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 java Java 21虚拟线程实战:用协程式编程彻底解决高并发I/O瓶颈 https://www.taomawang.com/server/java/2288.html

常见问题

相关文章

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

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