服务RejectedExecutionException问题对Semaphore的思考
目录
1. 问题背景
2. Java并发控制方式
3. Java限流控制方式
1. 问题背景
回放引擎服务隔一段时间就会发生一次服务拒绝,报错:Exception:RejectedExecutionException
通过服务监控平台查看stack信息,发现有很多线程waiting,点开waiting线程,可以看到具体是哪个类的哪一行出现了锁


阻塞信息解读:java.util.concurrent.Semaphore$NonfairSync@406f5dbf
分析:基本含义
java.util.concurrent.Semaphore是 Java 并发包中的一个信号量实现,用于控制同时访问某个资源的线程数量。NonfairSync表示该信号量采用非公平模式(即新来的线程可能优先于等待队列中的线程获得许可)。
阻塞场景分析
阻塞信息:java.util.concurrent.Semaphore$NonfairSync@406f5dbf
通常说明:
- 有线程在尝试获取 Semaphore 的许可时被阻塞,因为当前可用许可数量为 0。
- 非公平模式下,等待的线程不会严格按照先后顺序获得许可,可能出现“插队”现象。
- 堆栈或日志信息中出现该对象,通常是在分析线程阻塞或死锁时,定位到具体的同步资源。
典型应用场景
- 限流:限制同时执行的任务数(如连接池、批量任务处理)。
- 资源保护:如只允许 N 个线程同时访问某个临界区。
常见问题及排查建议
- 许可数设置过小
- 如果初始许可数太低,容易导致大量线程阻塞,影响系统吞吐。
- 未及时释放许可
- 某些线程获得许可后异常退出或忘记
release(),导致许可“泄漏”。
- 某些线程获得许可后异常退出或忘记
- 非公平模式下线程饥饿
- 长时间等待的线程可能一直得不到执行机会。
排查建议:
- 检查所有
acquire()后是否有对应的release()。 - 监控当前信号量的可用许可数(
semaphore.availablePermits())。 - 如需严格控制顺序,可考虑使用公平模式(
new Semaphore(permits, true))。
原因分析:
流量回放为了限制对回放服务的最大并发量,采用Semaphore方式控制API调用并发数,具体代码如下:可以看出第二个try 如果异常后,没有释放已获取的信号量,最终导致新的线程无法获取新的许可,系统阻塞。
public class ReplayService{private static final Semaphore semaphore = new Semaphore(32);public void replay(ReplayRequest request){try{semaphore.acquire();}catch(Exception e){return ResultUtil.error(-1,"获取许可失败")}try{//开始调用服务;}catch(Exception e){return ResultUtil.error(-1,"服务返回异常")}try{//回放结果处理;}catch(Exception e){return ResultUtil.error(-1,"结果处理失败")}finally{semaphore.release();}}
}
2. Java并发控制方式
并发控制主要是为了 协调多个线程安全、高效地访问共享资源,避免冲突、死锁或性能下降。
常见方式如下:
(1) 线程同步机制
-
synchronized
Java 关键字,保证同一时间只有一个线程执行同步代码块。 -
ReentrantLock
可重入锁,功能比 synchronized 更丰富(支持公平锁、条件变量等)。 -
ReadWriteLock(ReentrantReadWriteLock)
读写分离锁,允许多个线程同时读,但写操作互斥。 -
StampedLock
支持乐观读锁,性能更高,适合读多写少场景。
(2) 信号量(Semaphore)
-
控制同时访问某个资源的线程数量,适合限制并发连接数45。
(3) 倒计时器(CountDownLatch)
-
等待多个任务完成后再继续执行,常用于多线程任务的协调。
(4)( 栅栏(CyclicBarrier)
-
多线程到达屏障点后统一执行下一步,适合分阶段任务。
(5) 原子类(AtomicInteger、AtomicLong等)
-
基于 CAS 操作的无锁并发控制,适合高性能计数器等。
(6) 并发集合
-
ConcurrentHashMap、CopyOnWriteArrayList 等线程安全集合。
(7)线程池(ExecutorService)
-
控制任务执行线程数,避免频繁创建/销毁线程。
Semaphore(信号量限流)
- 本质是并发访问数量控制工具,用一个计数器(许可数)限制同时访问某个资源的线程数。
- 当许可用完时,其他线程会阻塞等待,直到有线程释放许可。
- 常用于限制最大并发量,例如数据库连接池、并发下载任务、API调用并发数等。
回放引擎为了控制对回放服务的速度,采用Semaphore方式,限制并发数32个线程同时调用服务
3. Java限流控制方式
限流是为了 控制请求速率或并发量,防止系统过载。
常见方式如下:
(1)基于 RateLimiter 的速率限制
-
Guava 提供的 RateLimiter 类,基于令牌桶算法限制 QPS(每秒请求数)12。
(2)固定窗口计数器
-
在固定时间窗口内统计请求数,超出限制则拒绝请求。
(3)滑动窗口算法
-
将时间窗口分成多个小格,平滑统计请求数,避免固定窗口的突发问题2。
(4)漏桶算法(Leaky Bucket)
-
以固定速率处理请求,平滑流量6。
(5)令牌桶算法(Token Bucket)
-
按固定速率生成令牌,请求需获取令牌才能执行,允许一定突发流量2。
(6)分布式限流
-
基于 Redis、ZooKeeper 等实现跨节点限流,适合分布式系统。
事后总结:服务拒绝报错时,以为是服务负载太大导致,查看cpu和gc情况,并没有异常,定位到方法后,让AI分析方法中存在的异常,存在严重的信号量泄漏问题。

