semaphore 是并发数控制而非限流,它限制同时执行的线程数,不控制单位时间请求数(qps);适用于同步阻塞服务保护下游资源,不适用于异步模型或短耗时接口。

为什么 Semaphore 不是“限流”而是“并发数控制”
很多人一看到接口限流就直接上 Semaphore,结果压测时发现 QPS 没降、超时却变多——根本原因是搞混了概念:Semaphore 控制的是**同时执行的线程数**,不是单位时间请求数(QPS)。它适合保护下游资源不被并发打垮(比如数据库连接池、本地缓存写入),但对防刷、削峰这类真正“限流”场景效果有限。
典型误用:给一个 HTTP 接口方法加 semaphore.acquire(),却没考虑异步 Servlet、线程复用(如 Tomcat 的 NIO 线程池)、或 Spring WebFlux 的非阻塞模型——这时 acquire() 可能阻塞在 IO 线程上,反而拖垮整个线程池。
- 适用场景:同步阻塞式服务(如传统 Spring MVC + Tomcat 同步模式)
- 不适用场景:WebFlux、Vert.x、gRPC 异步服务;或接口本身耗时极短(Semaphore 的 CAS 开销反而成瓶颈
- 关键判断点:你的请求处理逻辑是否「必然在同一线程内完成」?如果不是,别硬套
Semaphore
Semaphore 初始化必须用 false(非公平模式)
默认构造函数 new Semaphore(permits) 实际等价于 new Semaphore(permits, false),但很多人会下意识写成 new Semaphore(permits, true)——这是个隐蔽陷阱。公平模式会维护一个 FIFO 队列,所有 acquire() 请求排队等待,吞吐量直接掉 30%~50%,且在高并发下容易出现“长尾获取”:某个请求卡在队列中间,等几十毫秒才拿到许可,用户体验断崖式下跌。
真实压测数据:100 并发下,非公平模式平均获取延迟
立即学习“Java免费学习笔记(深入)”;
- 永远显式声明
new Semaphore(10, false),别依赖默认 - 如果业务真需要“先到先得”,优先考虑用
RateLimiter+ 时间窗口,而不是靠Semaphore公平性 - 注意:公平性开关只影响
acquire(),不影响tryAcquire(long, TimeUnit)的超时行为
必须配合 tryAcquire 和超时,不能无脑 acquire
acquire() 是无限等待,一旦后端响应变慢或 release() 被遗漏(比如异常没 catch 到),许可就会永久泄漏,Semaphore 迅速变成“空转红灯”——所有新请求卡死,监控看不出来,排查要翻日志找哪条链路没 release。
正确姿势是:所有 acquire 必须带超时,且 release 放在 finally 块里。
if (!semaphore.tryAcquire(100, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Too many requests");
}
try {
// 执行业务
} finally {
semaphore.release();
}- 超时值建议设为接口 SLO 的 1/3(比如 P95 是 300ms,这里设 100ms)
- 不要用
tryAcquire()无参版本——它不保证原子性,在极端竞争下可能返回true却实际没拿到许可 - 如果业务逻辑里有嵌套调用、异步分支,确保每个分支都覆盖
release,否则必漏
和 Spring AOP 结合时,切点必须选对方法签名
用 @Around 切 Controller 方法时,如果切的是 public 方法但实际被 CGLIB 代理(比如类没实现接口),AOP 可能失效;更常见的是切了 HandlerMethod 或 HttpServletRequest,结果 Semaphore 在 Filter 层就被拿走了,Controller 根本没机会执行。
最稳的方式是切具体业务 Service 方法,且该方法必须是 public + 由 Spring 容器管理的 bean。
- 错误示范:
@Pointcut("execution(* com.example.controller..*.*(..))")—— Controller 层太薄,异常可能抛在拦截器之后 - 推荐切点:
@Pointcut("execution(* com.example.service..*Service.*(..))") - 务必检查
@EnableAspectJAutoProxy(proxyTargetClass = true)是否开启,否则 JDK 动态代理对没有接口的类无效 - 切记:AOP 代理对象里的
this不是 Spring Bean,所以不能在切面里直接调this.method(),否则绕过代理,Semaphore彻底失效
复杂点在于,如果你的业务方法里又调了另一个加了同样切面的方法,得确认 Semaphore 是按“每方法独立计数”还是“整条调用链共用”——前者要每个方法配独立 Semaphore,后者得用 ThreadLocal + 可重入设计,但那就不是原生 Semaphore 能解决的了。










