Java 21 虚拟线程(Virtual Threads)

Java 21 虚拟线程(Virtual Threads)

虚拟线程是 JDK 21 通过 JEP 444​ 正式引入的轻量级线程实现,源自 Project Loom 项目。一句话定义:

A virtual thread is an instance of java.lang.Threadthat is not tied to a particular OS thread.

换句话说,虚拟线程是 JVM 管理的"用户态线程",不再是操作系统线程的 1:1 映射


一、为什么需要虚拟线程

传统 Java 线程(现称"平台线程")有两个痛点 :

  • 1:1 绑定 OS 线程 :每条 java.lang.Thread都是 OS 线程的薄包装,在其生命周期内独占底层 OS 线程
  • 资源昂贵:默认栈 1MB 左右,创建/切换成本高,单机通常只能撑几千条

这迫使开发者为了高并发不得不写异步/响应式代码(CompletableFuture、Reactor、WebFlux),代价是代码复杂度陡增、调试困难、栈轨迹支离破碎。

虚拟线程的目标就是:让你用最简单的"每请求一线程"同步写法,跑出接近异步的吞吐量​ 。

💡 一个很好的类比是虚拟内存:OS 用"大虚拟地址空间 → 少量物理 RAM"的映射制造了内存充裕的假象;JVM 则用"大量虚拟线程 → 少量 OS 线程"的映射制造了线程充裕的假象 。


二、核心机制:挂载(Mount)与卸载(Unmount)

虚拟线程并非脱离平台线程运行,而是运行在一组称为 carrier threads(载体线程) ​ 的平台线程之上。载体线程池默认是 ForkJoinPool,大小等于 CPU 核数 。

调度流程

  1. 虚拟线程就绪 → 被挂载(mount) 到某个载体线程上开始执行
  2. 遇到阻塞 I/O / Thread.sleep()/ 锁等待 → JVM 自动将其卸载(unmount) ,载体线程立即释放
  3. 阻塞操作完成 → 虚拟线程回到就绪队列,被任意可用载体线程重新挂载,从挂起点继续执行

关键差异对比

维度 平台线程 虚拟线程
底层载体 1 个 OS 线程(独占) 多个虚拟线程共享少量 OS 线程
内存占用 ~1MB 固定栈 几百字节起步,动态增长
阻塞行为 阻塞 → 浪费整个 OS 线程 阻塞 → 自动卸载,载体线程被复用
并发规模 几千 百万级
创建成本 高(内核态分配) 极低(用户态)

三、三种创建方式

scss 复制代码
// 方式 1:快捷创建并启动
Thread.startVirtualThread(() -> {
    System.out.println("Hello from " + Thread.currentThread());
});

// 方式 2:Builder 模式(可命名、可配置)
Thread vt = Thread.ofVirtual()
    .name("worker-", 0)       // 名称前缀
    .start(() -> doWork());

// 方式 3:Executor(推荐用于批量任务)
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 1_000_000; i++) {
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));  // 阻塞的是虚拟线程,载体线程不阻塞
            return "done";
        });
    }
}  // 关闭时自动等待所有任务完成

⚠️ 虚拟线程永远不应该被池化。JEP 444 明确指出:虚拟线程廉价且充足,应为每个任务创建新虚拟线程;而平台线程昂贵,才需要池化复用 。


四、适用场景与不适用场景

✅ 适合

  • 高并发 I/O 密集型:HTTP 请求、数据库查询、Redis 调用、消息消费
  • Web 服务器每请求一线程模型:Tomcat、Jetty、Spring MVC 等
  • 批量 I/O 任务:如 10 万条数据同步、并行调用多个外部服务

❌ 不适合

  • CPU 密集型任务 :图像处理、加密、大数据计算------瓶颈在 CPU 核数,不在线程数,应使用传统的 ForkJoinPool
  • 长时间运行的无限循环:会让载体线程调度器"饿死"
  • synchronized 块内长时间阻塞 :会导致线程固定(pinning) ,载体线程无法释放
  • JNI / Native 方法调用:虚拟线程会被固定到载体线程上

五、关于 "Pinning"(线程固定)

这是 Java 21 虚拟线程最关键的坑:当虚拟线程满足以下条件时,它无法被卸载,载体线程会跟着一起阻塞 :

  • synchronized块/方法内发生阻塞
  • 执行 native 方法

Java 21 中的应对策略

csharp 复制代码
// ❌ 危险:synchronized 内阻塞会导致 pinning
synchronized(lock) {
    Thread.sleep(1000);  // 载体线程被"固定",无法服务其他虚拟线程
}

// ✅ 推荐:使用 ReentrantLock
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    Thread.sleep(1000);  // 虚拟线程可被正常卸载
} finally {
    lock.unlock();
}

诊断命令:-Djdk.tracePinnedThreads=full,会在发生 pinning 时打印栈轨迹 。

📌 好消息:Java 24+ 已对此进行了显著改善 ,但 Java 21 仍是当前许多企业的生产基线,需要特别注意。


六、与 Spring Boot 3.2+ 的衔接

回到上一轮我们提到的配置,在 Java 21+ 环境下:

yaml 复制代码
spring:
  threads:
    virtual:
      enabled: true

开启后,Spring Boot 会自动将以下组件切换到虚拟线程 :

  • Tomcat / Jetty 的请求处理线程
  • @Async方法执行器
  • @Scheduled调度器
  • Kafka / RabbitMQ 监听器

但要注意:数据库连接池(HikariCP 等)仍然必要------数据库的连接数瓶颈在于数据库自身,不在于 Java 线程。虚拟线程只保证"等待连接时不会占用载体线程",但连接池大小仍需合理设置 。


七、一个直观的性能参照

Oracle Helidon 4.0 在生产环境采用虚拟线程后 :

  • 高并发场景下内存减少 90%
  • I/O 密集型工作负载吞吐量提升 5 倍
  • 代码库简化:用同步阻塞 I/O 替换了原有的响应式代码

Apache Tomcat 10.1+ 的基准测试:5 万并发用户下仅需 2GB RAM(平台线程模型需要 16GB),p95 延迟降低 45% 。


八、核心要点回顾

  1. 虚拟线程是 JVM 管理的轻量级 java.lang.Thread,不 1:1 绑定 OS 线程
  2. 它通过挂载/卸载机制将大量虚拟线程复用到少量载体线程上
  3. 不是"更快的线程",而是"更便宜、更多的线程" ------解决的是伸缩性问题,不是延迟问题
  4. 适合 I/O 密集型高并发;不适合 CPU 密集型
  5. 不要池化虚拟线程 ,用 newVirtualThreadPerTaskExecutor()
  6. 警惕 synchronized导致的 pinning,优先用 ReentrantLock

虚拟线程的本质,是把"线程"这种本应廉价的抽象,从操作系统手里拿回来交给了 JVM------让开发者重新拥有"一个请求一条线程"这种直观、易调试的编程模型,同时不失高并发能力。这也是为什么 Spring Boot 3.2 将其作为一等公民支持的原因。

相关推荐
Csvn1 小时前
🐍 Day 5: Python 函数详解 — 参数、作用域与一等公民
人工智能·后端
wno7041 小时前
Spring Boot中使用Servlet
spring boot·后端·servlet
老郑聊AI业财智造1 小时前
Spring AI 技术架构与源码分析
java·人工智能·后端·spring·架构·软件工程
省长1 小时前
别人绕过我的网关直接调用资源服务怎么办?使用 Sa-Token 解决:网关转发鉴权、RPC调用鉴权
java·后端·开源
MetaLite1 小时前
SpringBoot异常处理-到底该转换还是继续抛-入口层与调用层不能一刀切
java·spring boot·后端
Liora_Yvonne2 小时前
不懂后端,只会 TypeScript,想独立做完整项目?这套全栈底座就是给前端准备的
前端·后端·全栈
前端Hardy2 小时前
GitHub 爆火!236K+ Star!一套 Skills 让 AI 按工程师方式写代码
前端·后端
名字还没想好☜2 小时前
Go 用 slices/maps 标准库泛型函数:告别手写 Contains、Sort、去重(Go 1.21)
开发语言·后端·算法·golang·go
程序大爆炸2 小时前
gcsfuse与中断FUSE_INTERRUPT
后端