JDK21中虚拟线程引发类死锁问题:代码执行异常冻结
虚拟线程下同步锁导致冻结的原因分析
先看问题中的测试代码:
public class Test { private static final Object lock = new Object(); public static void main(String[] args) { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 100; i++) { int taskId = i; executor.submit(() -> performTask(taskId)); } } System.out.println("All tasks completed."); } private static void performTask(int id) { System.out.printf("Task %d started. Thread: %s%n", id, Thread.currentThread()); synchronized (lock) { System.out.printf("Task %d acquired lock. Thread: %s%n", id, Thread.currentThread()); try { Thread.sleep(Duration.ofSeconds(2)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } System.out.printf("Task %d finished. Thread: %s%n", id, Thread.currentThread()); } }
为什么会出现冻结?
核心问题出在虚拟线程的调度机制和Thread.sleep的行为上:
Thread.sleep不会释放平台线程
虚拟线程的优势是执行可挂载操作时会释放绑定的平台线程,让其他虚拟线程复用。但Thread.sleep()是例外——它会直接让虚拟线程绑定的平台线程进入睡眠状态,不会释放这个平台线程。平台线程池资源耗尽
Executors.newVirtualThreadPerTaskExecutor()默认使用的平台线程池大小等于CPU可用处理器数(比如4核机器就是4个平台线程)。提交100个虚拟线程任务后:- 前4个虚拟线程抢占到平台线程,其中一个先拿到
synchronized锁,调用Thread.sleep(2s)后占住平台线程。 - 剩下3个虚拟线程尝试拿锁失败,进入阻塞队列,但
synchronized阻塞属于不可挂载阻塞,它们会一直占着平台线程等待锁。 - 此时4个平台线程全被占满:1个持有锁sleep,3个等待锁阻塞。剩下的96个虚拟线程没有平台线程可用,无法尝试获取锁。
- 当第一个sleep的虚拟线程醒来释放锁后,它的平台线程会唤醒一个等待锁的虚拟线程,但新拿到锁的虚拟线程又会调用
Thread.sleep继续占住平台线程。循环往复,所有平台线程都卡在sleep或等待锁状态,整个executor彻底卡住,表现为类似死锁的冻结。
- 前4个虚拟线程抢占到平台线程,其中一个先拿到
关键区分:虚拟线程的可挂载与不可挂载阻塞
虚拟线程只有在执行可挂载操作时才会释放平台线程,比如:
Object.wait()LockSupport.park()- Java 19+支持的NIO阻塞操作
- 异步API(如
CompletableFuture)
而synchronized阻塞、Thread.sleep()都属于不可挂载的阻塞,会持续占用平台线程。
修复方案
把Thread.sleep(Duration.ofSeconds(2))换成虚拟线程可挂载的睡眠操作,比如用LockSupport.parkNanos:
// 替换原sleep代码 LockSupport.parkNanos(Duration.ofSeconds(2).toNanos());
这样虚拟线程在睡眠时会释放平台线程,其他等待锁的虚拟线程就能拿到平台线程尝试获取锁,代码会按预期串行执行。
内容的提问来源于stack exchange,提问作者Strayer_xx
相关产品推荐
相关产品推荐

