LockSupport.unpark先于park调用时park偶发阻塞的原因
问题根因
这个现象和JDK实现bug无关,本质是对LockSupport的许可机制、CountDownLatch的底层实现逻辑存在认知偏差:
- LockSupport为每个线程维护的许可(permit)是线程级全局唯一的,只有0、1两个状态,不存在“不同park调用对应独立许可”的规则。线程在任何代码位置(包括JDK内部源码逻辑)调用
LockSupport.park(),都会检查这个全局许可:如果许可为1就消耗许可直接返回,如果为0就进入阻塞;任何位置调用unpark(目标线程)都会把目标线程的许可置为1,和后续park是谁调用的没有关系。 CountDownLatch.await()基于AQS框架实现,底层阻塞唤醒完全依赖LockSupport:线程调用await()时会先判断计数器值,不为0就调用LockSupport.park()阻塞;即使被意外唤醒(比如提前调用的unpark),也会自旋重新校验计数器值,只要计数器没到0就会再次调用park()阻塞,直到计数器归零才会从await()返回。
偶发阻塞的核心原因是:你提前调用unpark发放的许可,根本没有留存到你手写的那行LockSupport.park(),而是在CountDownLatch.await()的执行过程中被提前消耗了。触发阻塞的典型时序如下:
- t1线程启动后打印
t1 started,进入c.await()逻辑,检查到计数器值为1,随即调用park()准备阻塞在await阶段 - 主线程执行
LockSupport.unpark(t1),将t1的许可置为1,直接唤醒阻塞在await上的t1,这个许可当场就被消耗 - t1被唤醒后自旋检查计数器,发现主线程还没执行
c.countDown(),计数器值仍为1,于是再次调用park()阻塞在await阶段,此时许可已经回到0状态 - 主线程继续执行,打印
unpark...后调用c.countDown(),将计数器置为0,同时调用unpark(t1)唤醒t1 - t1被唤醒后检查到计数器为0,从await()方法返回,打印
park...后执行你手写的LockSupport.park() - 此时countDown发放的许可已经被await阶段的park消耗,没有可用许可,t1自然就阻塞在了你手写的park方法上。
没有出现阻塞的运行结果,是因为线程调度走到了另一种时序:主线程执行unpark和countDown时,t1还没进入await()内部的park逻辑,t1在await()中检查到计数器已经是0,根本不会执行park操作,提前发放的许可没有被消耗,就会留存到你手写的park调用时直接返回,不会阻塞。两种时序的触发完全由操作系统的线程调度顺序决定,因此问题是偶发的。
验证方式:将代码中的
c.await()替换为不依赖LockSupport实现的阻塞逻辑(比如短时间的Thread.sleep()),保证unpark调用和你手写的park调用之间没有其他park操作消耗许可,运行时就100%不会出现阻塞,完全符合Javadoc的描述。
内容的提问来源于stack exchange,提问作者asiuf爱施德
相关产品推荐
相关产品推荐

