Semaphore 直译为信号。实际上 Semaphore 可以看做是一个信号的集合。不同的线程能够从 Semaphore 中获取若干个信号量。当 Semaphore 对象持有的信号量不足时,尝试从 Semaphore 中获取信号的线程将会阻塞。直到其他线程将信号量释放以后,阻塞的线程会被唤醒,重新尝试获取信号量。

信号量模型(P/V 操作)由 Dijkstra 提出,是操作系统课的经典内容;Java 的 Semaphore 把它落成了 AQS 共享模式的一个应用。本文讲清三件事:AQS 层如何实现许可计数、公平与非公平的差异、以及它在限流场景的正确用法与常见误用。

模型:许可(permits)池

Semaphore 维护一组许可:

  • acquire():取一个许可,无许可则阻塞(P 操作)
  • release():还一个许可,可能唤醒等待者(V 操作)
  • 构造参数 permits 决定同时允许多少个线程通过:permits=1 时退化为互斥锁;permits=N 时允许 N 个并发
Semaphore sem = new Semaphore(3);   // 最多 3 个线程同时访问资源
sem.acquire();
try {
accessLimitedResource(); // 数据库连接、外部 API、许可证资源
} finally {
sem.release(); // 必须 finally 归还,否则许可泄漏
}

AQS 实现:共享模式的状态计数

Semaphore 内部 Sync 继承 AQS,state 即剩余许可数:

  • acquire → acquireSharedInterruptibly(1):CAS 把 state 减 1;减后为负(不足)则入队 park 阻塞
  • release → releaseShared(1):CAS 把 state 加 1,并唤醒等待队列头部线程(共享模式传播唤醒)
  • 支持一次获取多个许可:acquire(n),state 减 n

与 CountDownLatch(state 只减到 0 开门)不同,Semaphore 的 state 可增可减、循环使用,这是”资源池”语义与”门闩”语义在 AQS 上的分野。

公平与非公平

  • 非公平(默认):新到线程直接 CAS 抢许可,可能”插队”成功,吞吐高但等待者可能饥饿
  • 公平:new Semaphore(n, true),acquire 前先检查等待队列是否有排队者(hasQueuedPredecessors),有则乖乖排队,FIFO 保证不饥饿

限流场景若要求”先到先得、不饿死”,用公平模式;追求极限吞吐且容忍偶发长等待,用非公平。

限流场景与误用

正确用法:

  • 保护有限资源:数据库连接数、下游 API 并发配额、硬件设备通道——permits 设为资源容量
  • 降级限流:接口入口用 Semaphore 限制并发执行数,超限请求快速失败或排队等待(配合 tryAcquire 超时)
  • 批量任务控速:爬虫/导出任务控制同时进行的下载数

常见误用:

  • release 与 acquire 不成对:异常路径漏 release,许可逐渐耗尽直至全阻塞;必须 finally
  • 用 Semaphore 做业务计数:它是并发控制原语,不是计数器(计数用 LongAdder/CHM)
  • permits 设过大失去保护意义:限流值应来自下游容量压测,而非拍脑袋
  • acquire 后、业务代码前抛异常:确保 acquire 成功才进入 try 块(acquire 放 try 外或捕获中断正确处理)

与同类原语定位

  • ReentrantLock:互斥(1 个许可且绑定持有者,可重入)
  • Semaphore(1):互斥但不绑定持有者(任何线程可 release,可用于”信号”式协作)
  • Semaphore(N):并发限流
  • RateLimiter(Guava):速率限流(QPS 维度),与 Semaphore 的并发数限流互补

Semaphore 的原理一句话:AQS 共享模式下”可增可减的 state 计数 + 不足即排队”。它的价值不在复杂,而在把”资源容量”这个业务约束直接表达为并发原语——用对了,它是限流与资源保护最轻量的正解。