ManagedBlocker 不适用于虚拟线程,因其专为 ForkJoinPool 平台线程设计,而虚拟线程由 JVM 自动挂起/恢复;应使用 Thread.Blocker.begin()/end() 显式标记阻塞点以优化载体线程调度。

不能靠 ForkJoinPool.ManagedBlocker 解决虚拟线程的阻塞停滞问题——它压根不适用于虚拟线程场景。
为什么 ManagedBlocker 对虚拟线程无效
ManagedBlocker 是为 ForkJoinPool 中的平台线程(即普通 Thread)设计的机制,用于在执行可能阻塞的任务时触发“补偿线程”(compensate),避免整个池被卡死。但虚拟线程(Virtual Thread)根本不走 ManagedBlocker 路径:它们由 JVM 直接调度,阻塞时自动挂起并让出底层载体线程(Carrier Thread),无需手动标记“我要阻塞了”。调用 ManagedBlocker.block() 在虚拟线程中只会抛出 UnsupportedOperationException 或静默失效。
- 虚拟线程的阻塞感知是 JVM 内置行为,与
ForkJoinPool.ManagedBlocker无任何代码路径交集 -
ManagedBlocker的block()方法要求实现类必须被提交到ForkJoinPool执行;而虚拟线程默认不提交任务,而是直接由 JVM 调度器接管 - 试图在虚拟线程里 new 一个
ManagedBlocker并传给pool.managedBlock(),会导致该操作退化为同步阻塞,反而破坏虚拟线程的轻量性
真正该用 Blocker.begin()/end(),而不是 ManagedBlocker
Java 21+ 提供了专为虚拟线程设计的阻塞标记 API:java.lang.Thread.Blocker(注意不是 ForkJoinPool.ManagedBlocker)。它的作用是向 JVM 显式声明:“接下来这段代码可能会阻塞”,从而影响载体线程的动态扩容策略。
- 仅当底层载体线程是
CarrierThread类型时,Blocker.begin()才会触发 ForkJoinPool 的线程补偿逻辑(类似ManagedBlocker的效果) - 必须严格配对使用:
boolean attempted = Blocker.begin(); try { ... } finally { Blocker.end(attempted); },否则可能泄漏状态或导致 carrier 线程无法释放 - 它不改变代码行为,只影响 JVM 对“是否需要额外 carrier 线程”的判断——比如在大量虚拟线程同时做数据库查询时,能避免因 carrier 不足导致的排队延迟
常见误用:把虚拟线程往 ForkJoinPool 里 submit
有人试图这样“绕过”问题:
Thread.ofVirtual().start(() -> {
ForkJoinPool.commonPool().submit(() -> {
// 阻塞 IO
TimeUnit.SECONDS.sleep(1);
}).join();
});这反而更糟:
- 外层是虚拟线程,内层又提交到
ForkJoinPool,造成双重调度开销 - 阻塞发生在
ForkJoinPool的平台线程上,不再是虚拟线程挂起,彻底失去百万并发能力 - 如果
ForkJoinPool的 parallelism 很小(如默认cpu-1),30 个这样的任务会串行执行,耗时翻 30 倍
该怎么做:让阻塞自然发生,必要时加 Blocker 标记
绝大多数情况,你什么都不用做——虚拟线程遇到 Object.wait()、Thread.sleep()、文件读写、Socket 阻塞调用等,JVM 自动挂起并复用 carrier 线程。只有在以下情况才需主动干预:
- 调用的是 JNI 或未被 JVM 检测到的阻塞方法(如某些老版本 NIO Channel 的
read()) - 观察到 carrier 线程长期满载、虚拟线程排队等待 carrier(可通过
jcmd <pid> VM.native_memory summary或 JFR 查看CarrierThread数量与活跃度) - 明确知道该段代码会长时间阻塞(>100ms),且并发量极大(万级虚拟线程)
此时才加标记:
boolean attempted = Thread.Blocker.begin();
try {
// 这里放确定会阻塞的代码,比如 legacy blocking DB driver call
legacyDbDriver.query("SELECT * FROM huge_table");
} finally {
Thread.Blocker.end(attempted);
}真实瓶颈往往不在阻塞本身,而在 carrier 线程数量不足或第三方库未适配虚拟线程——比如用了老版本 HikariCP 或没开启 jdk.net.usePlainSocketImpl=false。这些比纠结 ManagedBlocker 重要得多。

















