Virtual Threads 深入

共 28 题
📑 题目列表 28 题
#
★★★

1. 虚拟线程下仍需用信号量/连接池限制并发的资源(DB 连接、下游配额)的原因

虚拟线程下,为什么仍需用信号量/连接池限制 DB 连接、下游配额等资源的并发?

  • 虚拟线程的轻量与资源限制的差异
  • 有界资源(连接、配额)的并发控制
  • 信号量与连接池的作用

虚拟线程非常轻量,可创建数百万个,但虚拟线程执行时仍会占用真实的有界资源:数据库连接、下游服务的配额、文件句柄、内存等。创建大量虚拟线程同时等待同一个 DB 连接池,会导致连接池耗尽、线程阻塞排队,甚至引发下游超时雪崩。因此仍需用信号量、连接池(如 HikariCP)或配额限制来约束对这类有界资源的并发访问,即使线程本身轻量。虚拟线程解决的是"线程数"的瓶颈,而非"资源数"的瓶颈。

虚拟线程消除了"线程数"限制,但资源(连接、配额)仍是硬上限。若不加限制,共享的有界资源会被海量虚拟线程争抢,导致阻塞与失败。信号量/连接池是"资源级节流",与虚拟线程的"线程级轻量"互补。

#
★★★

2. 虚拟线程与 ReentrantLock 的协作

虚拟线程与 ReentrantLock 如何协作?

  • ReentrantLock 在虚拟线程下的挂起
  • 锁获取时的 unmount
  • 与 synchronized 的对比

虚拟线程与 ReentrantLock 协作良好:当虚拟线程在 ReentrantLock 上阻塞等待锁时,JVM 会挂起(unmount)该虚拟线程,释放载体线程(carrier thread),让载体线程去执行其他虚拟线程,从而实现高并发下的高效调度。这得益于 ReentrantLock 基于 j.u.c 的阻塞机制被 Loom 集成,可挂起虚拟线程。相比之下,synchronized 在 JDK 24 前会钉住载体线程(pinning),JEP 491 后 synchronized 也不再钉住。ReentrantLock 是虚拟线程场景下推荐的锁,因为它提供可中断、可超时、公平性等 synchronized 没有的能力。

虚拟线程的价值在于"阻塞时可让出载体线程"。ReentrantLock 的阻塞能被 JVM 挂起线程,因而与虚拟线程高度适配。生产上优先用 ReentrantLock 而非 synchronized 以获得更好的取消与诊断能力。

#
★★★

3. 虚拟线程在 CompletableFuture 的链路

虚拟线程在 CompletableFuture 的链路中如何工作?

  • CompletableFuture 的异步执行
  • 虚拟线程作为执行器
  • 阻塞与回调的配合

CompletableFuture 的异步操作(thenApplyAsync、supplyAsync 等)默认使用 ForkJoinPool.commonPool,也可指定执行器。将虚拟线程执行器(Executors.newVirtualThreadPerTaskExecutor())传入 CompletableFuture,可让每个异步任务在独立虚拟线程上执行,避免共享线程池的固定线程数限制。在 CompletableFuture 链路中,虚拟线程可执行阻塞 I/O 而不会阻塞载体线程,提升并发度。不过 CompletableFuture 基于回调,与虚拟线程的"阻塞式"思维不同,需按场景选择:若链路中有大量阻塞 I/O,用虚拟线程 + 阻塞同步更直观;若纯计算回调,用 CompletableFuture 即可。

虚拟线程与 CompletableFuture 在"异步执行 + 阻塞 I/O"场景互补。指定虚拟线程执行器可绕过 commonPool 的线程数上限,但结构化并发(StructuredTaskScope)在复杂的父子任务编排上比 CompletableFuture 更清晰。

#
★★★

4. 虚拟线程在 Netty 的协作

虚拟线程在 Netty 中如何协作?

  • Netty 的事件循环模型
  • 虚拟线程与 Netty 的集成
  • 阻塞与异步的取舍

Netty 基于事件循环(EventLoop)与异步非阻塞 I/O。虚拟线程在 Netty 中的协作方式:将 Netty 的 EventLoop 或业务 handler 的执行放到虚拟线程上,避免阻塞事件循环线程。Netty 官方强调"不要在事件循环线程中做阻塞操作",虚拟线程可让 handler 中偶发的阻塞操作(如调用 DB、外部服务)在虚拟线程上执行,而不阻塞事件循环。但 Netty 的异步模型本身高效,虚拟线程主要用于"把阻塞式业务代码塞进异步框架"的桥接,需使用 Netty 提供的虚拟线程集成(如 executor 传入)。取舍上,若业务代码是阻塞式调用,用虚拟线程 + 阻塞式 handler 更简单;若全异步,保持 Netty 原生模型。

Netty 与虚拟线程的协作核心是"避免阻塞事件循环"。虚拟线程作为事件循环的补充执行器,处理阻塞型业务,保留 Netty 高性能的异步 I/O。JDK 21 起虚拟线程成熟,Netty 4.1.100+ 支持虚拟线程集成。

#
★★★

5. 虚拟线程在 Spring Boot 4.x 的虚拟线程启用

虚拟线程在 Spring Boot 4.x 中如何启用?

  • Spring Boot 4.x 的虚拟线程配置
  • 虚拟线程的启用方式
  • 与内置容器的配合

Spring Boot 4.x 支持通过配置属性启用虚拟线程:设置 spring.threads.virtual.enabled=true 即可让内置容器(Tomcat、Jetty、Undertow)与执行器使用虚拟线程。Spring Boot 4.x 将虚拟线程作为一等公民,默认提升对虚拟线程的支持,包括 AsyncTaskExecutor、Scheduling 等。启用后,每个请求/任务在独立虚拟线程上处理,避免固定线程池的线程数限制。Spring Boot 4 还支持 spring.mvc.async 等场景的虚拟线程。Spring Boot 3.2 起已有该配置,4.x 进一步强化。

启用虚拟线程可大幅提升高并发、阻塞 I/O 场景的吞吐。Spring Boot 4.x 通过配置开关即可接入,无需改代码,但需注意有界资源(连接池、DB)仍需限制。

#
★★★

6. 虚拟线程在 Tomcat/Jetty/Undertow Servlet 容器(Spring Boot 3.2+)

虚拟线程在 Tomcat/Jetty/Undertow Servlet 容器(Spring Boot 3.2+)中如何工作?

  • Servlet 容器的线程模型
  • 虚拟线程在容器中的接入
  • 各容器的支持差异

从 Spring Boot 3.2 起,Tomcat、Jetty、Undertow 都支持虚拟线程:配置 spring.threads.virtual.enabled=true 后,容器将每个请求分派到虚拟线程上执行,替代传统的固定线程池(如 Tomcat 的 max-threads)。这样 Servlet 处理中的阻塞 I/O(DB 调用、外部 API)不再受限于容器线程数,吞吐大幅提升。三个容器通过各自的虚拟线程执行器支持,Spring Boot 统一封装。切换时需注意:虚拟线程下容器线程池参数(max-threads、accept-count)的语义变化,以及有界资源的并发控制。

Servlet 容器传统上用固定线程池限制并发,虚拟线程打破该限制。Spring Boot 3.2+ 统一提供虚拟线程开关,使请求处理规模化。生产需评估连接池、下游依赖的承载能力。

#
★★★

7. 虚拟线程的 mount/unmount 与栈管理

虚拟线程的 mount/unmount 与栈管理机制是什么?

  • mount/unmount 的概念
  • 栈的保存与恢复
  • 与载体线程的关系

mount/unmount 是虚拟线程在载体线程(carrier thread)上执行与挂起的机制。当虚拟线程被调度到载体线程上运行时叫 mount,从载体线程上挂起(被阻塞、让出)叫 unmount。unmount 时,虚拟线程的栈(Continuation)被保存到堆中的 Continuation 对象,载体线程的栈被释放,可执行其他虚拟线程;再次 mount 时恢复栈继续执行。栈管理由 Loom 的 Continuation 实现,虚拟线程的调用栈不在线程栈上,而是堆中可增长的 Continuation 对象。这使虚拟线程的内存占用小、可迁移,也意味着深调用栈的虚拟线程可能占用较多堆内存。

mount/unmount 与 Continuation 栈是虚拟线程的基石:栈在堆中,挂起/恢复不依赖 OS 线程栈,从而支持海量虚拟线程。代价是频繁的 mount/unmount 有调度开销,且深栈消耗堆内存。

#
★★★

8. 虚拟线程的 pinning 在 synchronized 块 vs ReentrantLock 下的不同表现(JEP 491 前后)

虚拟线程的 pinning 在 synchronized 块 vs ReentrantLock 下的不同表现是什么?JEP 491 前后有何变化?

  • pinning 的概念
  • synchronized 与 ReentrantLock 的差异
  • JEP 491 的改变

pinning(钉住)指虚拟线程在等待某个锁时,其载体线程被锁定无法让出,导致其他虚拟线程无法使用该载体线程,降低并发。JEP 491 前(JDK 24 之前):synchronized 块在虚拟线程阻塞等待时会导致 pinning,载体线程被钉住;而 ReentrantLock(j.u.c)不会钉住,虚拟线程可挂起让出载体线程。JEP 491(JDK 24)引入"可钉住虚拟线程的同步"(monitor 的偏置/可停顿机制),使 synchronized 也不再钉住载体线程,从而消除 synchronized 的 pinning 问题。临床表现:JEP 491 后,synchronized 与 ReentrantLock 在虚拟线程下的挂起行为趋于一致,但 ReentrantLock 仍提供可中断、超时等能力。

pinning 是虚拟线程性能杀手,JEP 491 通过让 monitor 支持挂起(不再钉住)解决了 synchronized 的 pinning。此前建议用 ReentrantLock 规避;JEP 491 后两者都不钉住,但 ReentrantLock 的灵活性仍使其成为推荐。

#
★★

9. 虚拟线程的异常处理与 JDK 25 结构化并发(StructuredTaskScope)

虚拟线程的异常处理与 JDK 25 结构化并发(StructuredTaskScope)如何工作?

  • 虚拟线程的异常传播
  • StructuredTaskScope 的异常收集
  • 失败处理策略

虚拟线程的异常处理:未捕获异常会传播到虚拟线程的退出点,可被 Thread.getUncaughtExceptionHandler 处理。结构化并发(StructuredTaskScope)提供更结构化的异常处理:fork 的子任务在作用域内运行,父线程通过 await 或 join 收集结果与异常。受检异常需包装,未检查异常通过 scope 的 join 传播。StructuredTaskScope 提供 ShutdownOnFailure(任一失败即取消其余)与 ShutdownOnSuccess(任一成功即取消其余)策略,异常处理集中在父作用域,避免子任务异常吞没或泄漏。JDK 25 中结构化并发仍为预览(JEP 505 为第五次预览,JDK 26 的 JEP 525 为第六次预览),与虚拟线程配合实现任务编排的异常一致性。

结构化并发的核心价值是"异常与取消的确定性传播"。相比裸 virtual thread 的各自为政,StructuredTaskScope 让父子任务的生命周期、异常和取消统一管理,配合虚拟线程实现可预测的并发编排。

#
★★

10. 虚拟线程的栈结构(Continuation)在 JDK 25 中的内存占用与 ForkJoinPool 调度细节

虚拟线程的栈结构(Continuation)在 JDK 25 中的内存占用与 ForkJoinPool 调度细节是什么?

  • Continuation 的内存占用
  • ForkJoinPool 的调度
  • 栈的启动与增长

虚拟线程的 Continuation 栈默认以较小初始大小(如几十 KB)启动,按需增长(最多约 1MB 或按 -Djdk.virtualThreadStackSize 配置),内存占用远小于平台线程(1MB 以上)。栈动态增长使浅栈虚拟线程内存极低,深栈则消耗堆内存。调度上,虚拟线程由 ForkJoinPool 调度(默认 parallel 线程数),工作窃取(work stealing)机制让空闲的载体线程从其他线程队列窃取就绪的虚拟线程。ForkJoinPool 的调度细节包括:虚拟线程的 yield、park/unpark 通过 ForkJoinPool 的任务队列管理,载体线程数默认等于 CPU 核数。

动态栈 + ForkJoinPool 工作窃取是虚拟线程高效的核心。内存占用低(Continuation 栈)与调度灵活(窃取)让海量虚拟线程成为可能,但深栈与频繁调度会带来额外开销。

#
★★

11. 虚拟线程与 JNI(Native)的限制

虚拟线程与 JNI(Native)有哪些限制?

  • JNI 调用与载体线程
  • native 阻塞的 pinning
  • 受限的 JNI 方法

虚拟线程的限制:调用 JNI 原生方法时,虚拟线程会钉住(pin)其载体线程,因为原生代码期望在线程栈上稳定执行,无法被挂起/恢复。JEP 472(JDK 24)对 JNI 受限方法发出警告,包括从虚拟线程调用那些会钉住载体线程的 JNI 方法(如 Class.getDeclaredField 相关、某些 native 方法)。若原生方法阻塞(如阻塞的网络调用),载体线程会被钉住,无法执行其他虚拟线程,导致并发下降。限制:应避免在虚拟线程中调用可能阻塞的 JNI 方法,或在原生层使用可让出(如 JDK 25 的 FFM 协作)的方式。

JNI 与虚拟线程的冲突源于原生代码依赖稳定的线程栈。JEP 472 的警告暴露了这类调用,指导开发者识别并迁移到 FFM(Foreign Function & Memory API)或避免阻塞 JNI。

#
★★

12. 虚拟线程与平台线程的内存占用对比

虚拟线程与平台线程的内存占用有何对比?

  • 平台线程的栈内存
  • 虚拟线程的 Continuation 栈
  • 内存对比与数量上限

平台线程每个约 1MB 栈内存(默认),且受 OS 线程数限制(几千到几万);虚拟线程的 Continuation 栈动态增长,初始占用小(几十 KB),浅栈时内存占用极低,可创建数十万甚至数百万个。内存对比关键:平台线程栈是固定预留的,虚拟线程栈是按需分配在堆中。因此虚拟线程能承载远超平台线程的并发量,但深栈虚拟线程会占用较多堆内存,需注意 -Xmx 的堆配置。

内存占用是虚拟线程数量规模的决定因素。平台线程栈固定预留导致内存浪费与数量上限;虚拟线程动态栈让数量以内存为约束而非线程数,这是其"百万并发"的物理基础。

#
★★

13. 虚拟线程在数据库连接池的边界(HikariCP 虚拟线程兼容)

虚拟线程在数据库连接池的边界是什么?HikariCP 与虚拟线程的兼容性如何?

  • 连接池的有界性
  • HikariCP 与虚拟线程
  • 连接池配置的调整

虚拟线程在数据库连接池的边界:即使虚拟线程数量巨大,DB 连接池仍是有界的(如 HikariCP 默认 maximumPoolSize=10)。虚拟线程在获取连接时若池耗尽会阻塞等待,海量虚拟线程同时等待连接池会导致请求排队与超时。HikariCP 与虚拟线程兼容:虚拟线程在 HikariCP 的获取连接阻塞上可被挂起(不钉住载体线程),因此连接池的连接获取不会阻塞载体线程。但需合理配置连接池大小与获取超时,避免虚拟线程数远超连接数导致的排队。边界上,应让虚拟线程数受限于有界资源(连接数),而非无限。

连接池是虚拟线程下的主要瓶颈资源。HikariCP 的获取连接等待可被虚拟线程挂起(不钉住),但仍需控制连接池大小与获取超时,防止虚拟线程洪峰淹没连接池。

#
★★

14. 虚拟线程的 Thread.ofVirtual() 工厂方法与 Executors.newVirtualThreadPerTaskExecutor() 的差异与协作

Thread.ofVirtual() 工厂方法与 Executors.newVirtualThreadPerTaskExecutor() 的差异与协作是什么?

  • Thread.ofVirtual() 的创建方式
  • newVirtualThreadPerTaskExecutor 的批量创建
  • 两者的协作

Thread.ofVirtual() 是创建单个虚拟线程的工厂方法(如 Thread.ofVirtual().name("vt").start(runnable)),用于直接创建并启动一个虚拟线程。Executors.newVirtualThreadPerTaskExecutor() 返回一个执行器,为每个提交的任务创建一个新的虚拟线程,适合批量任务与现有 Executor 接口的兼容。差异:ofVirtual 直接控制单个线程,newVirtualThreadPerTaskExecutor 面向任务提交与执行器抽象。协作:两者底层都是虚拟线程,可混用;newVirtualThreadPerTaskExecutor 便于把原有固定线程池执行器替换为虚拟线程执行器,而 ofVirtual 适用于显式创建单个虚拟线程(如配合结构化并发)。

两者是虚拟线程的"单线程"与"执行器"两种创建入口。配合使用可让既有代码平滑迁移到虚拟线程,同时保留细粒度控制。

#
★★

15. 虚拟线程的载体线程(Carrier Thread)机制

虚拟线程的载体线程(Carrier Thread)机制是什么?

  • 载体线程的概念
  • 挂载与调度
  • 载体线程数

载体线程(carrier thread)是执行虚拟线程的底层平台线程。虚拟线程不直接运行,而是被调度到载体线程上执行;一个载体线程可顺序执行多个虚拟线程。载体线程通过 ForkJoinPool 管理,默认数量等于可用 CPU 核数。虚拟线程执行时"挂载"到载体线程,阻塞时"卸载"(unmount),让载体线程执行其他虚拟线程。载体线程机制使虚拟线程的并发不受线程数限制,而是受载体线程(CPU)数量的调度。载体线程本身是平台线程,其创建与销毁由 JVM 管理。

载体线程是虚拟线程与硬件之间的桥梁。CPU 核数决定载体线程数,虚拟线程在载体线程上调度执行,实现"轻量线程 + 复用 OS 线程"。

#
★★

16. 虚拟线程的阻塞 I/O 如何在 JDK 层面挂起(socket/file/锁),哪些原生阻塞无法挂起

虚拟线程的阻塞 I/O 如何在 JDK 层面挂起(socket/file/锁),哪些原生阻塞无法挂起?

  • 可挂起的阻塞 I/O
  • 不可挂起的原生阻塞
  • JDK 层挂起机制

JDK 层面的阻塞 I/O(socket 读写、通道操作、j.u.c 锁、Thread.sleep、阻塞队列)在虚拟线程下会被 handler 挂起:虚拟线程阻塞时 unmount,载体线程被释放,I/O 完成后再恢复。这依赖 JDK 对阻塞 I/O 的拦截(如 NIO 的 poller)。不可挂起的原生阻塞:调用原生方法(JNI)中的阻塞操作(如某些 C 库的阻塞调用、文件锁的某些情况)无法被 JDK 拦截,会钉住载体线程。此外,某些系统调用(如文件系统锁)在部分平台可能钉住。JDK 24 的 JEP 472 警告会暴露这些钉住场景。

挂起的核心是"JDK 能拦截的阻塞"。socket/锁/睡眠等 JDK 管理的阻塞可挂起;原生栈外阻塞无法挂起,只能钉住。识别并避免原生阻塞是虚拟线程性能的关键。

#
★★

17. 虚拟线程在 JDK Flight Recorder 的诊断事件

虚拟线程在 JDK Flight Recorder(JFR)中有哪些诊断事件?

  • JFR 的虚拟线程事件
  • 诊断内容
  • 生产可观测性

JFR 提供多个虚拟线程诊断事件:jdk.VirtualThreadStart(虚拟线程启动)、jdk.VirtualThreadEnd(结束)、jdk.VirtualThreadPinned(钉住,含钉住时长与位置)、jdk.VirtualThreadSubmitFailed(提交失败)等。这些事件记录虚拟线程的创建、挂起、钉住与调度情况,帮助诊断虚拟线程的性能问题(如大量 pinning、调度阻塞)。JFR 的 VirtualThreadPinned 事件可配置 threshold,用于生产中采样钉住事件。

JFR 是虚拟线程可观测性的主力。通过分析虚拟线程事件,可定位钉住、调度瓶颈与异常,是生产调优虚拟线程应用的关键工具。

#
★★

18. 虚拟线程的 Thread.sleep 与调度

虚拟线程的 Thread.sleep 与调度机制是什么?

  • Thread.sleep 在虚拟线程下的行为
  • 睡眠的挂起与恢复
  • 调度精度

虚拟线程调用 Thread.sleep 时,会挂起(unmount)该虚拟线程,释放载体线程,让载体线程执行其他虚拟线程;睡眠结束后由调度器重新调度到载体线程执行。因此虚拟线程上的 sleep 不会阻塞载体线程,支持海量虚拟线程同时睡眠。JDK 25 的 Thread.sleep 精度改进对虚拟线程的睡眠调度也有益,短时睡眠的精度提升让虚拟线程调度更公平。睡眠由 JVM 的调度器管理,恢复后进入 ForkJoinPool 队列等待执行。

Thread.sleep 在虚拟线程下是"可挂起的阻塞",与平台线程的 sleep(占用 OS 线程)不同。这让虚拟线程下的定时/延迟任务更高效。

#
★★

19. 虚拟线程的 Thread.start/Thread.join 语义

虚拟线程的 Thread.start/Thread.join 语义是什么?

  • 虚拟线程的 start
  • join 的等待与取消
  • 与平台线程的差异

虚拟线程的 Thread.start 启动虚拟线程执行,与平台线程类似,但 start 更轻量(创建与调度开销小)。Thread.join 让当前线程等待虚拟线程结束;虚拟线程的 join 会挂起调用线程(若调用线程是虚拟线程则让出载体线程),直到被等待的虚拟线程完成。join 支持超时(join(millis))。与平台线程的差异:虚拟线程 join 是轻量的、可挂起的,不像平台线程 join 占用 OS 线程。虚拟线程的 join 语义保证执行顺序,但也提倡用结构化并发(StructuredTaskScope.await)替代裸 join 以获得异常与取消管理。

start/join 在虚拟线程下保持 Thread 语义但更轻量。结构化并发推荐用 StructuredTaskScope 提供更健壮的等待与结果收集,而非手动 join。

#
★★

20. -Djdk.tracePinnedThreads 与 JFR 排查 pinning 的完整流程(watch/trace 事件)

-Djdk.tracePinnedThreads 与 JFR 排查 pinning 的完整流程是什么?

  • -Djdk.tracePinnedThreads 参数
  • JFR 的 VirtualThreadPinned 事件
  • 排查流程

排查虚拟线程 pinning 的流程:1)用 -Djdk.tracePinnedThreads=full 或 =short 启动 JVM,在虚拟线程被钉住时打印钉住位置(栈),full 打印完整栈,short 打印精简栈;2)同时启用 JFR 的 jdk.VirtualThreadPinned 事件(可配置 threshold),记录钉住事件的发生与堆栈;3)分析产生的日志/JFR 事件,定位钉住的具体代码(如 synchronized 块、JNI 调用、libc 阻塞);4)修复:用 ReentrantLock 替代 synchronized(JEP 491 后已改进)、迁移 JNI 到 FFM、避免原生阻塞。JEP 491 后 synchronized 不再钉住,但 JNI/原生阻塞仍会钉住,-Djdk.tracePinnedThreads 与 JFR 事件是定位这些残留边界的关键。

tracePinnedThreads 用于开发期快速定位,JFR 用于生产期采样。两者结合形成"开发期定位 + 生产期监控"的 pinning 排查闭环。

#
★★

21. JEP 491(Synchronize Virtual Threads without Pinning,JDK 24 标准)

JEP 491 如何实现"不钉住虚拟线程的 synchronized"?

  • JEP 491 的目标
  • monitor 的改进
  • 对 synchronized 的影响

JEP 491(JDK 24)实现"Synchronize Virtual Threads without Pinning",让虚拟线程在 synchronized 块中阻塞等待监视器(monitor)锁时不再钉住载体线程。此前虚拟线程在 synchronized 上阻塞会钉住载体线程,导致并发下降。JEP 491 通过让 monitor 在被竞争时支持虚拟线程的挂起(unmount),使虚拟线程在等待 synchronized 锁时释放载体线程,等待其他线程释放锁后再恢复。JEP 491 在 JDK 24 成为标准,消除了 synchronized 在虚拟线程下的主要 pinning 来源。

JEP 491 是虚拟线程生态的关键改进,使 synchronized 与 ReentrantLock 在虚拟线程下的挂起行为一致。但它不解决 JNI/原生阻塞的钉住,那些仍是残留边界。

#
★★

22. JFR 的 jdk.VirtualThreadPinned 事件的 threshold 配置与生产环境采样策略

JFR 的 jdk.VirtualThreadPinned 事件的 threshold 配置与生产环境采样策略是什么?

  • VirtualThreadPinned 事件的 threshold
  • 生产采样策略
  • 避免性能影响

jdk.VirtualThreadPinned 事件在虚拟线程被钉住时触发,可通过 threshold 配置(如 -XX:StartFlightRecording 或 JFR 配置)控制记录钉住时长阈值,只有钉住超过该阈值的才记录,避免噪声。生产环境采样策略:默认可能关闭或在阈值较高时启用,因为钉住事件频繁且记录栈有开销。建议:先以较高 threshold(如 20ms)采样,观察钉住频率与位置,若有钉住再降低 threshold 或结合 -Djdk.tracePinnedThreads 定位。阈值配置权衡"完整性"与"性能开销",生产环境应采样式启用。

threshold 控制采样粒度,避免高频钉住事件淹没 JFR 并减轻开销。生产策略是"先粗后细":高阈值发现存在钉住,再针对性降低阈值或开启 trace 定位。

#
★★

23. newVirtualThreadPerTaskExecutor 与固定线程池在吞吐与资源上的差异(吞吐量模型)

newVirtualThreadPerTaskExecutor 与固定线程池在吞吐与资源上有何差异?吞吐量模型是什么?

  • 固定线程池的吞吐模型
  • 虚拟线程的吞吐模型
  • 资源占用差异

吞吐量模型:固定线程池的吞吐受线程数(N)限制,处理阻塞 I/O 时,吞吐约等于 N / (单请求耗时),线程数需兼顾阻塞等待;虚拟线程执行器(newVirtualThreadPerTaskExecutor)为每个任务创建虚拟线程,吞吐几乎不受线程数限制,只受 CPU 与资源约束,阻断 I/O 场景下虚拟线程吞吐显著高于固定线程池。资源差异:固定线程池占用固定数量的 OS 线程与栈内存,虚拟线程仅按需占用动态 Continuation 栈,内存占用随栈深浅变化。换算上,虚拟线程让"线程数"不再成为吞吐瓶颈,但需注意有界资源(连接池)与 CPU 的约束。

高并发阻塞 I/O 场景下,固定线程池的线程数成为吞吐天花板,虚拟线程打破该模型。吞吐量模型变化的核心是"用内存换线程数",但 CPU 与有界资源仍是最终约束。

#
★★

24. 虚拟线程的调试与线程转储(jcmd Thread.dump_to_file 的虚拟线程视图)

虚拟线程的调试与线程转储(jcmd Thread.dump_to_file 的虚拟线程视图)如何工作?

  • jcmd Thread.dump_to_file
  • 虚拟线程的转储视图
  • 调试技巧

jcmd Thread.dump_to_file 可生成线程转储,默认包含虚拟线程视图。JDK 21+ 的 jcmd Thread.dump_to_file 支持 -format=json 与 -format=text,并包含虚拟线程的栈信息(识别虚拟线程与载体线程)。调试时,可通过 jcmd 转储查看虚拟线程的挂起位置、钉住情况与运行状态。虚拟线程的转储中,Thread 对象标记虚拟线程,可区分其调度状态。此外 jcmd Thread.dump_to_file 支持只转储虚拟线程或平台线程,便于定位虚拟线程的性能与阻塞问题。

线程转储是排查虚拟线程问题的核心手段。jcmd 的虚拟线程视图展示每个虚拟线程的栈与状态,配合 -Djdk.tracePinnedThreads 与 JFR 可完整定位虚拟线程问题。

#
★★

25. 虚拟线程(Virtual Threads,JEP 444)的 ForkJoinPool 调度

虚拟线程(JEP 444)的 ForkJoinPool 调度机制是什么?

  • JEP 444 的虚拟线程
  • ForkJoinPool 的调度
  • 工作窃取

JEP 444(JDK 21)将虚拟线程定稿。虚拟线程由 ForkJoinPool 调度,默认 parallel 线程数等于可用 CPU 核数(可用 -Djdk.virtualThreadScheduler.parallelism 调整)。调度采用工作窃取(work stealing):每个载体线程有自己的任务队列,空闲的载体线程从其他线程的队列尾部窃取就绪的虚拟线程执行。虚拟线程被阻塞(park、sleep、I/O)时 unmount,从队列移除;恢复(unpark)时重新入队。ForkJoinPool 的调度保证载体线程的利用效率,让少量载体线程承载海量虚拟线程。

ForkJoinPool 工作窃取是虚拟线程调度的核心,实现负载均衡与高效利用。载体线程数默认等于 CPU 核数,虚拟线程在它们之间动态调度。

#

26. JEP 491 修复同步块钉住后,虚拟线程 pinning 的残留边界(原生方法/JNI 阻塞)与官方建议

JEP 491 修复同步块钉住后,虚拟线程 pinning 的残留边界是什么?官方建议是什么?

  • JEP 491 后的残留边界
  • 原生方法/JNI 阻塞
  • 官方建议

JEP 491 修复了 synchronized 的钉住,但残留边界仍存在:调用原生方法(JNI)时,如果原生代码阻塞(如 C 库的阻塞调用、文件锁、某些 JDBC 驱动的原生实现),虚拟线程会钉住载体线程,因为原生栈无法挂起/恢复。官方建议:避免在虚拟线程中调用可能阻塞的 JNI 方法;若必须使用原生阻塞,将其隔离到平台线程或专用执行器;用 FFM(Foreign Function & Memory API)替代 JNI 以获得更好的协作;通过 JEP 472 的警告与 -Djdk.tracePinnedThreads 识别残留钉住。JEP 491 也保留了对少数受限场景(如通过受支持的 API 的 pinning)的说明。

原生阻塞是 JNI 与虚拟线程的固有冲突,无法通过 monitor 修复。官方建议是识别、隔离并迁移,避免钉住影响载体线程利用率。

#

27. 虚拟线程与 Pin 检测的 JFR 事件

虚拟线程与 Pin 检测的 JFR 事件是什么?

  • Pin 检测事件
  • JFR 的诊断
  • 生产监控

JFR 提供 jdk.VirtualThreadPinned 事件用于检测虚拟线程钉住(pinning)。当虚拟线程被钉住(如 JNI 阻塞或 JEP 491 前的 synchronized)时,该事件触发,记录钉住时长、线程与堆栈。可用 threshold 配置(如 -XX:StartFlightRecording=settings=... 或 jfr 配置)过滤短钉住。JFR 的 Pin 检测让生产环境能监控虚拟线程钉住情况,定位影响载体线程利用率的代码。结合 jdk.VirtualThreadStart/End 等事件,可全面分析虚拟线程生命周期与钉住。

JFR 的 VirtualThreadPinned 事件是生产环境监控虚拟线程钉住的关键。通过事件频率与堆栈,识别钉住热点并优化。

#

28. 虚拟线程的 getId 与 threadId(JDK 25)

虚拟线程的 getId 与 threadId(JDK 25)是什么?

  • Thread.getId 与 threadId
  • 虚拟线程的 ID 分配
  • 区别

Thread.getId() 返回线程的标识符,JDK 19 起 Thread 新增 threadId() 方法(等价于 getId())。虚拟线程的 ID 是在创建时分配的长整型标识,可能为 0 或负数(虚拟线程 ID 从 0 开始)。JDK 25 中,virtual thread 的 ID 可能为 0 或负数(如 -1 表示尚未分配),与平台线程 ID 从 1 开始不同。threadId() 与 getId() 等价,用于线程的唯一标识。虚拟线程的 ID 不保证唯一性(可能复用),但通常用于日志与诊断。

虚拟线程的 ID 机制与平台线程类似但初始值不同(可能为 0/负数)。理解 ID 分配有助于日志分析,但虚拟线程的标识更应依赖 Thread 对象与 name。