Java中Semaphore与调度器竞态条件致自定义限流器许可数超限问题
问题原因
导致Semaphore许可数超过设定上限,核心是许可补全逻辑的非原子性+多补全任务并发执行,具体触发场景如下:
schedulePermitReplenishment方法是public修饰,若外部代码不小心多次调用该方法,每次调用都会新建一个独立的单线程调度器,同时启动一个新的许可补全定时任务。多个补全任务并行执行时,就会出现超量发放许可的问题:
举个例子,maxPermits=100,当前可用许可为90,两个补全任务同时执行:- 任务A读取可用许可为90,计算得到需要补充10个许可
- 任务B同时读取可用许可为90,同样计算得到需要补充10个许可
- 任务A先执行
release(10),可用许可变为100 - 任务B再执行
release(10),可用许可直接变为110,超过上限
Semaphore本身的设计没有对许可总数做上限校验,只要调用release方法就会增加许可数,因此多补全任务并发时必然会出现许可数超标的情况。
后续限流器失效的原因
当可用许可数超过maxPermits后,下一次补全任务计算maxPermits - semaphore.availablePermits()会得到负数,而Semaphore的release方法传入负数参数会直接抛出IllegalArgumentException。ScheduledExecutorService的定时任务如果抛出未捕获的异常,后续的调度会直接终止,不会再继续补全许可,最终许可被业务线程消耗完之后,限流器就完全失效了。
修复方案
// 1. 将schedulePermitReplenishment改为private,禁止外部调用 private void schedulePermitReplenishment() { scheduler = Executors.newScheduledThreadPool(1); scheduler.scheduleWithFixedDelay(() -> { // 2. 加try-catch避免异常导致调度终止 try { int gap = maxPermits - semaphore.availablePermits(); // 3. 仅当需要补充的许可数为正时才执行release if (gap > 0) { semaphore.release(gap); } } catch (Exception e) { // 可在此处添加异常日志打印 } }, 0, 1, timePeriod); }
如果需要更严谨的防护,可以额外添加一个原子状态变量,标记补全任务是否已经启动,避免重复初始化调度器。
内容的提问来源于stack exchange,提问作者vpork
相关产品推荐
相关产品推荐

