LockSupport 的 permit 不能累计:两次通知要靠状态保存
- 发布时间:2026-10-23 23:16:31
- 本文热度:浏览 2 赞 0 评论 0
- 文章标签: Java LockSupport 线程同步
- 全文共1字,阅读约需1分钟
连续unpark两次,不会给线程攒下两张以后可用的通行证。LockSupport为每个线程维护的permit最多只有一个;已经有permit时,再unpark不会继续加一。把它当成事件计数器,消费者就可能只醒来一次,却不知道曾经发生两次业务变化。
这项限制与调用次数是否成功返回无关。unpark本身没有返回一个“累计通知数量”,park也不返回“本次究竟消费了第几条事件”。如果业务要知道待处理数量,应当把数量存在独立的原子状态、队列或其他受保护数据里,唤醒只负责提醒线程重新查看。
一张permit与两条工作记录
下面程序使用Java 17。第一段在已经启动的线程真正park以前调用unpark,验证不必严格先等待再通知;第二段把两条业务事件记入pending,再唤醒工作者,最终按照状态完成两次处理。
保存为PermitIsNotCount.java,执行java PermitIsNotCount.java:
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.LockSupport;
public class PermitIsNotCount {
static void await(CountDownLatch latch)throws InterruptedException {
if(!latch.await(3,TimeUnit.SECONDS))throw new IllegalStateException("coordination timeout");
}
public static void main(String[] args)throws Exception {
CountDownLatch started=new CountDownLatch(1);
AtomicBoolean go=new AtomicBoolean(),firstReturned=new AtomicBoolean();
Thread probe=new Thread(()->{
started.countDown();
while(!go.get())Thread.onSpinWait();
LockSupport.park();
firstReturned.set(true);
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(40));
},"permit-probe");
probe.start();await(started);
LockSupport.unpark(probe);LockSupport.unpark(probe);go.set(true);
probe.join(3000);
if(probe.isAlive()){probe.interrupt();throw new AssertionError("probe stuck");}
System.out.println("pre-unpark first returned="+firstReturned.get());
System.out.println("second park has timeout; return cause not inferred");
AtomicInteger pending=new AtomicInteger(),processed=new AtomicInteger();
AtomicBoolean closed=new AtomicBoolean();
CountDownLatch ready=new CountDownLatch(1),done=new CountDownLatch(2);
Thread worker=new Thread(()->{
ready.countDown();
while(!closed.get()||pending.get()>0) {
if(Thread.currentThread().isInterrupted())return;
int available=pending.get();
if(available==0){LockSupport.park(pending);continue;}
if(pending.compareAndSet(available,available-1)) {
processed.incrementAndGet();done.countDown();
}
}
},"state-worker");
worker.start();
try {
await(ready);pending.addAndGet(2);
LockSupport.unpark(worker);LockSupport.unpark(worker);
await(done);
System.out.println("two events processed="+processed.get()+", pending="+pending.get());
if(processed.get()!=2||pending.get()!=0)throw new AssertionError("event loss");
}finally {
closed.set(true);LockSupport.unpark(worker);worker.join(3000);
if(worker.isAlive()){worker.interrupt();throw new IllegalStateException("worker active");}
}
}
}
pre-unpark first returned=true
second park has timeout; return cause not inferred
two events processed=2, pending=0
探针在第一次park返回后,又调用带上限的parkNanos。第二次返回可能来自等待期结束、中断或其他允许的原因,程序没有根据一次耗时断言“肯定消耗了第二张permit”。permit不累计来自API合同,不应靠一条容易受调度影响的秒表测试来替代。
第一段用短暂自旋把位置固定在park之前,避免在两次unpark与目标park之间又插入另一个可能内部使用park的阻塞同步器。它只服务这个受控探针,不是建议真实服务用忙循环协调启动。线程尚未启动时调用unpark也不能被当成可靠通知,样例明确先等到线程进入运行路径。
第二段则不依赖两次唤醒能否分别被观察。pending一次增加二,即使unpark合并,工作者也会循环处理到零。processed与pending断言检查业务事件完整性,而不是检查线程醒了几次。需要传递真实任务时可以换成受控队列,不能把计数器里没有保存的载荷凭空恢复出来。
为什么一定要先记录状态,再通知
如果先unpark,工作者醒来检查仍然没有任务,再次park;生产者随后才写入任务,又没有新的唤醒,就可能留下无人处理的工作。先发布业务状态,再发通知,让等待循环有清楚的检查依据。
样例使用AtomicInteger与AtomicBoolean表达可见性和原子更新。不能把它们简单换成几个没有同步的普通字段,再因为两边调用了unpark就默认所有访问安全。LockSupport用于构建同步组件,需要与组件自己的内存协议配合,单独调用不是一份通用数据发布合同。
检查pending等于零与调用park之间,生产者也可能恰好插入任务。因为生产者会先增加pending再unpark,permit能让随后的park直接返回,再次检查状态;这正是permit机制要覆盖的一类竞争窗口。它解决通知与等待时序,不替业务状态计数。
工作者通过CAS认领一条工作,成功后才增加processed。若以后增加多个工作者,不能把认领改成先get再set,否则会重复认领;另一方面,当前计数演示没有定义多个任务载荷与各自结果,不能仅增加线程数就宣称实现了完整任务系统。
park返回不是条件已经成立
park允许没有显式通知也返回,线程中断也会使等待结束,因此醒来以后必须回到循环重查。不能把park放进if,只检查一次pending,再无条件拿走一个不存在的任务;也不能把一次唤醒直接解释成“配置已经加载完成”。
中断时park不抛InterruptedException,也不会像某些阻塞方法那样替调用者清除标志。示例发现中断就结束工作,不在中断状态下继续无任务park,避免每次立即返回造成空转。对外需要传播取消时,应由所属接口明确表达,不能依赖一个没有异常返回值的方法让上层猜。
park传入pending作为blocker,只是让诊断工具更容易识别线程阻塞关联对象;它不是取得这份对象的监视器锁,也不等同于synchronized。把blocker换成另一个描述对象,不会改变permit数量或建立额外互斥。
停止同样是一项状态变化
关闭时先设置closed,再unpark,让已经停在空等待中的工作者有机会看到退出条件。仅写closed却不唤醒,线程可能一直等不到下一条业务通知;只unpark却不更新closed,又会被循环判断送回等待。
本例选择先处理完已认领范围再正常关闭,中断则允许直接退出。真实组件若要求清空、排空或返回未完成任务,应给出自己的停止合同。permit不保存业务历史,进程退出后也不会恢复工作记录;需要可靠执行仍要有独立持久来源与结果确认。
普通业务代码通常更适合现成的阻塞队列、信号量或锁条件。只有确实需要构建自己的等待组件时,才直接使用LockSupport,并把状态、不变量与退出路径一起验证。两次通知可能合并并不可怕,可怕的是系统没有别的地方记录那两次真正要做的事。