在 Java SDK 中,Semaphore 是信号量模型的具体实现。
操作系统中的信号量模型
操作系统中的信号量模型,可概括为 “一个计数器,一个等待队列,三个方法”:
- init():设置计数器的初始值;
- down():计数器的值减 1;
- 当计数器值 < 0,阻塞当前线程;
- 当计数器值 >= 0,不做其他操作。
- up():计数器的值加 1;
- 当计数器值 <= 0,唤醒等待队列中的一个线程,并将其移出队列;
- 否则,不做其他操作。
这些方法都是原子性的,其原子性是由信号量模型的实现方保证的。
其中 down()、up() 在历史上最早称为 P 操作和 V 操作,所以信号量模型也被称为 PV 原语。
信号量模型中,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们。
Semaphore 类
public class Semaphore implements java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer { ... }
static final class NonfairSync extends Sync { ... }
static final class FairSync extends Sync { ... }
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
// ...
}
说明:
- AQS 的上层应用(借助 state 维护许可数量)
当作为互斥锁使用时:
- 可重入
- 公平性可选(默认是非公平)
方法:(建议通过 IDE,看看 Semaphore 的 Structure)
- acquire、acquireUninterruptibly、tryAcquire:申请资源(获取许可证)
- release:释放资源(释放许可)
应用场景
- 通常用于限制访问某些资源的线程数量。如:在限流场景下的限流器,用于连接池、对象池、线程池等。
(在 Semaphore 的 Java Doc 中给了一个 “使用信号量来控制对项目池的访问的类” 的示例)
- 当 Semaphore 的计数器为 1 时,可以用作互斥锁。这通常被称为 “二进制信号量”,因为它只有两种状态(可用 or 不可用)。因为信号量没有所有权的概念,这个 “锁” 可以由所有者以外的线程释放。这在一些特殊的环境中很有用,例如死锁恢复。
Note
- 因为 Semaphore 允许多个线程访问临界区,所以在临界区内的公共资源,还是要考虑线程安全性。有必要的话,还需要互斥(加锁)操作。
如:在对象池示例中,对象保存在了 Vector 中,Vector 是 Java 提供的线程安全的容器,如果我们把 Vector 换成 ArrayList,是否可以呢?
不可以! 因为 Semaphore 允许多个线程进入临界区,所以要保证 线程池 add、remove 的安全性,需要使用线程安全的容器进行管理。