Project Loom: carrier pinning触发原因与场景技术咨询
Java虚拟线程Carrier Pinning相关问题解答
1. 原生代码触发Carrier Pinning的核心原因
你提到的「必须从Java代码层面执行park才能完成carrier线程切换」的理解是不准确的。
虚拟线程能从carrier(承载虚拟线程运行的平台线程)上卸载、让carrier去执行其他虚拟线程的核心前提,是JVM可以完整掌控当前执行栈的状态,能够安全保存、恢复执行上下文。
当虚拟线程执行Java代码时遇到适配过的阻塞点(比如LockSupport.park()、已经适配虚拟线程的阻塞Socket IO等),JVM可以完整把Java栈的帧信息序列化存储到堆内存,安全卸载虚拟线程,不会pin住carrier。
但执行JNI原生方法时,执行流会进入JVM无法管控的原生栈区域:原生代码可能持有栈内存指针、操作当前平台线程的本地存储(TLS)、依赖当前线程的原生上下文,JVM既无法遍历保存原生栈的状态,也无法保证虚拟线程下次挂载到其他carrier时原生逻辑能正常执行。因此只要虚拟线程的执行栈里存在未返回的原生方法帧,JVM就会强制pin住当前carrier,不允许卸载虚拟线程,和有没有在Java层执行park没有关系——哪怕原生代码内部触发了阻塞,只要还没返回到Java层,carrier就会一直被占用。
2. File IO触发Carrier Pinning的原因
这和「Linux缺乏成熟异步File IO支持」没有直接关系,核心原因是JDK暂未完成File IO栈和虚拟线程调度的适配,且现有Linux异步IO机制和Java传统File API的语义存在适配成本:
- 虚拟线程能做到不pin carrier的阻塞操作,本质是把原本阻塞的系统调用改成了「非阻塞fd + 事件通知」模式:比如Socket IO会把fd设为非阻塞,没有数据时就park虚拟线程,等epoll等机制通知fd就绪后再唤醒,全程carrier可以释放去跑其他任务。但普通文件的fd天生不支持epoll等事件通知机制(epoll明确不支持常规文件的事件监听),没法复用Socket那套非阻塞适配逻辑。
- 早期Linux的libaio存在诸多限制:要求使用O_DIRECT模式绕过页缓存、缓冲区必须内存对齐,和Java File API默认依赖页缓存的读写语义完全不匹配,且历史上稳定性问题较多,无法直接用来适配。
- 新的io_uring虽然已经具备支持带页缓存的异步文件IO的能力,但JDK需要兼容更低版本的Linux内核,且重写整个File IO底层栈需要处理极多边界兼容问题,截止JDK 21/22的正式版本,这部分适配工作还未完成。当前File IO的read/write/open等操作最终都会走到未做虚拟线程yield适配的JNI阻塞调用,自然会和其他JNI场景一样pin住carrier。
3. 其他会触发Carrier Pinning的场景
除了JNI调用、未适配的File IO之外,常见的pinning场景还有以下几类:
- 虚拟线程进入
synchronized修饰的同步块/同步方法时,只要执行栈里还持有synchronized隐式锁,遇到阻塞点就会pin住carrier。原因是synchronized的监视器(monitor)状态是和当前承载的平台线程绑定的,如果强行卸载虚拟线程挂载到其他carrier,会导致锁持有者信息错乱,破坏锁的语义。java.util.concurrent.locks包下的显式锁已经做了虚拟线程适配,持有这类锁时遇到阻塞不会触发pinning。 - 调用
Thread.stop()、Thread.suspend()等早已废弃的线程控制方法时,JVM为了保证线程状态一致性,会强制pin住carrier,这类方法本身已经不推荐在任何场景使用,实际业务中很少碰到。 - 部分第三方库中未做虚拟线程适配的原生阻塞调用,比如老版本JDBC驱动里的原生网络请求、某些本地库的阻塞方法调用,本质和JNI调用pinning的逻辑一致——执行流进入了JVM无法管控上下文的原生代码,就会触发pinning。
- JVM运行到全局安全点(比如GC停顿、类重定义等场景)时,会短暂pin住所有carrier线程,这属于JVM内部运行时的正常行为,不是业务代码触发的异常情况。
内容的提问来源于stack exchange,提问作者Rahul
相关产品推荐
相关产品推荐

