Java虚拟线程实践
约 1293 字大约 4 分钟
javaconcurrencyloom
2026-03-14
概述
虚拟线程(Virtual Threads)是 Project Loom 带来的核心能力,目标是把“线程的编程模型”和“协程级别的资源成本”结合起来。它适合 IO 密集型任务,尤其适合大量请求同时阻塞等待数据库、缓存、HTTP 或消息系统的场景。
一句话理解:虚拟线程让你继续写同步代码,但不用再为线程数量和平台线程上下文切换成本付出同样高的代价。
为什么它重要
传统平台线程的典型问题:
- 每个线程都对应一个 OS 线程,创建和切换成本高。
- 线程池会把并发能力和池大小绑死,容量规划困难。
- 同步代码可读性更好,但大规模并发时容易被迫改成回调或响应式模型。
虚拟线程把这些约束拆开了:
- 线程数量可以远多于平台线程数量。
- 阻塞调用会尽量“卸载”到底层调度器,不长期占住平台线程。
- 每个请求一个线程的模型重新变得可行。
基本用法
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = new ArrayList<>();
for (String url : urls) {
futures.add(executor.submit(() -> httpClient.get(url)));
}
for (Future<String> future : futures) {
System.out.println(future.get());
}
}如果你只是想快速创建一个虚拟线程,也可以直接使用 Thread.ofVirtual():
Thread.startVirtualThread(() -> {
String profile = userService.loadProfile(42L);
System.out.println(profile);
});运行模型
关键点:
- 虚拟线程本身不是操作系统线程。
- JDK 调度器把多个虚拟线程复用到少量 carrier threads 上。
- 当虚拟线程进入可挂起的阻塞点时,carrier thread 可以被回收去执行其他任务。
最适合的场景
Web 请求处理
同步 Controller、同步 Service、同步 Repository 的写法会变得非常自然。以前担心线程池爆掉,现在可以用每请求一个虚拟线程的方式处理大并发。
批量远程调用
例如一个聚合接口同时调用多个下游服务,虚拟线程可以显著降低线程池编排复杂度。
任务编排
本地批处理、抓取器、数据同步器这类“高并发 IO + 逻辑串行”任务,使用虚拟线程通常比 CompletableFuture 链式拼接更容易维护。
不适合的场景
- CPU 密集型任务。虚拟线程不会凭空增加 CPU。
- 对延迟抖动极其敏感且存在大量长时间占用 CPU 的任务。
- 依赖旧版库且库内部使用 native 调用或不可挂起阻塞的场景。
一个典型的服务写法
public class OrderFacade {
private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
public OrderDetail queryOrderDetail(long orderId) throws Exception {
Future<Order> orderFuture = executor.submit(() -> orderRepository.findById(orderId));
Future<UserProfile> userFuture = executor.submit(() -> userClient.fetchProfile(orderId));
Future<List<Shipment>> shipmentFuture = executor.submit(() -> logisticsClient.query(orderId));
return new OrderDetail(
orderFuture.get(),
userFuture.get(),
shipmentFuture.get()
);
}
}这个写法的价值不在“语法更短”,而在于:
- 可读性接近串行代码。
- 错误处理仍然基于异常传播。
- 并发边界清晰,不需要在业务代码里塞大量回调组合。
Pinning 问题
虚拟线程并不是所有阻塞都可以无损卸载。最需要关注的是 pinning,也就是虚拟线程在某些情况下会长时间绑住 carrier thread。
常见触发点:
- 进入
synchronized临界区后执行长阻塞操作。 - native 调用无法被 Loom 友好挂起。
- 某些旧驱动或旧库内部仍然以平台线程语义工作。
反例:
synchronized (lock) {
Thread.sleep(Duration.ofSeconds(2));
jdbcTemplate.queryForObject(sql, String.class);
}更稳妥的做法是缩小临界区,把阻塞操作移到锁外,或者改用 ReentrantLock 等更容易控制的并发原语。和 AQS框架详解、synchronized底层机制 可以一起看。
迁移建议
- 先选一个 IO 密集接口做试点,不要全站一次切换。
- 排查 JDBC 驱动、HTTP Client、消息客户端版本,确认对 Loom 友好。
- 关注线程 dump、阻塞热点和载体线程利用率。
- 重新审视旧有线程池。很多地方可以从“固定线程池”退回“虚拟线程执行器”。
- 保持 CPU 密集任务仍走有限并行度的专用线程池。
与响应式的关系
虚拟线程不是响应式编程的替代品,但会缩小很多团队采用响应式的必要性。
- 如果你的主要痛点是“同步代码线程太贵”,虚拟线程通常更直接。
- 如果你的系统已经深度依赖背压、流式处理和事件驱动,响应式仍然有价值。
- 在团队协作层面,虚拟线程通常更容易落地,因为它保留了更熟悉的调用栈和调试方式。
总结
虚拟线程最核心的价值,不是更快,而是把高并发服务的实现复杂度降下来。对于典型后端服务,它让“简单的同步代码”重新成为大并发下的可选项。
贡献者
更新日志
9f6c2-feat: organize wiki content and refresh site setup于