一、核心概念
1. 什么是信号量?
- 信号量维护一组"许可证"(permits),线程在访问受保护资源前必须先获取一个许可证。
- 如果没有可用许可证,线程会阻塞(或立即返回失败,取决于调用方法),直到有其他线程释放许可证。
- 不实际创建"许可证对象",只是用一个整数计数器表示当前可用数量。
比如:停车场有 10 个车位(permits = 10)。每来一辆车(线程),要先"acquire"一个车位;离开时"release"归还车位。
2. 两种模式
- 非公平(Nonfair):默认模式。新来的线程可能插队获取许可证,即使有线程在等待("barging"行为),吞吐量高但可能导致饥饿。
- 公平(Fair):按 FIFO 顺序分配许可证,避免线程长时间等待,但性能略低。
3. 特殊用法:二元信号量(Binary Semaphore)
- 当 permits = 1 时,信号量退化为互斥锁(Mutex)。
- 关键区别 :普通锁(如 ReentrantLock)要求"谁加锁谁解锁",而信号量不要求所有权 ------任何线程都可以调用
release()。这在死锁恢复等特殊场景有用。
二、关键实现机制(基于 AQS)
Semaphore 内部使用 AbstractQueuedSynchronizer(AQS)实现同步逻辑:
java
private final Sync sync; // Sync 是 AQS 的子类
Sync 的两个子类:
NonfairSync:非公平版本,tryAcquireShared直接尝试 CAS 减少许可数。FairSync:公平版本,在尝试获取前先检查是否有排队线程(hasQueuedPredecessors()),如果有就直接返回 -1(表示需排队)。
核心方法:
| 方法 | 作用 |
|---|---|
acquire() |
获取 1 个许可,不可用则阻塞(可中断) |
release() |
释放 1 个许可,唤醒等待线程 |
tryAcquire() |
尝试获取,立即返回 true/false |
availablePermits() |
返回当前可用许可数(调试用) |
drainPermits() |
一次性拿走所有可用许可 |
所有操作底层都通过 AQS 的
state字段(int 类型)表示许可数量,使用 CAS 保证原子性。
三、典型使用场景
场景 1:限制并发访问数量
java
// 最多允许 3 个线程同时访问数据库连接池
Semaphore dbPool = new Semaphore(3, true); // 公平模式
public void accessDB() throws InterruptedException {
dbPool.acquire(); // 获取许可
try {
// 使用数据库连接
} finally {
dbPool.release(); // 归还许可
}
}
场景 2:资源池管理(如你代码中的 Pool 示例)
- 控制对有限资源(如对象池、文件句柄)的访问。
getItem()前 acquire,putItem()后 release。
场景 3:流量控制 / 限流
- 比如 Web 服务器限制每秒最多处理 100 个请求,可用信号量控制。
四、重要注意事项
-
许可数可以为负数
构造时传入负数,表示"欠债",必须先
release()多次才能让acquire()成功。 -
tryAcquire()不遵守公平策略即使设为公平模式,
tryAcquire()仍会"插队"获取可用许可。 -
内存可见性保证
release()之前的动作 happen-beforeacquire()之后的动作,确保线程间数据可见。 -
不要求"配对"使用
A 线程 acquire,B 线程 release 是合法的(虽然通常不推荐,容易出错)。
五、总结一句话
Semaphore是一个计数型的同步工具,通过控制"许可证"的发放与回收,限制同时访问某资源的线程数量,既可用于资源池管理,也可作为更灵活的锁机制。
如果你正在学习并发编程,建议结合 ReentrantLock、CountDownLatch、CyclicBarrier 对比理解,它们都是基于 AQS 的不同应用。