一、概述
Semaphore
(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
二、使用案例
可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。假如有一个需求,要读取几万个文件的数据,因为都是IO密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有10个,这时我们必须控制只有10个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用Semaphore来做流量控制。
/**
* 类说明:演示Semaphore用法,一个数据库连接池的实现
*/
public class SemaphoreExample {
private final static int POOL_SIZE = 10;
//两个指示器,分别表示池子还有可用连接和已用连接
private final Semaphore useful, useless;
//存放数据库连接的容器
private static LinkedList<Connection> pool = new LinkedList<Connection>();
//初始化池
static {
for (int i = 0; i < POOL_SIZE; i++) {
pool.addLast(SqlConnectImpl.fetchConnection());
}
}
public DBPoolSemaphore() {
this.useful = new Semaphore(10);
this.useless = new Semaphore(0);
}
/*归还连接*/
public void returnConnect(Connection connection) throws InterruptedException {
if (connection != null) {
System.out.println("当前有" + useful.getQueueLength() + "个线程等待数据库连接!!"
+ "可用连接数:" + useful.availablePermits());
useless.acquire();
synchronized (pool) {
pool.addLast(connection);
}
useful.release();
}
}
/*从池子拿连接*/
public Connection takeConnect() throws InterruptedException {
useful.acquire();
Connection connection;
synchronized (pool) {
connection = pool.removeFirst();
}
useless.release();
return connection;
}
}
三、实现原理
3.1 内部类
Semaphore
同样由AQS
实现,总共有三个内部类,并且三个内部类是紧密相关的,Semaphore
与ReentrantLock
的内部类的结构相同,类内部总共存在Sync
、NonfairSync
、FairSync
三个类,NonfairSync
与FairSync
类继承自Sync
类,Sync
类继承自AbstractQueuedSynchronizer
抽象类。
//内部类,继承自AQS
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
// 构造函数,初始化同步资源
Sync(int permits) {
// 设置状态数
setState(permits);
}
// 获取许可数
final int getPermits() {
return getState();
}
// 非公平策略获取同步资源
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
// 剩余的许可
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))// 许可小于0或者有许可直接CAS尝试设置state的值
return remaining;
}
}
// 共享模式下进行释放
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
// 可用的许可
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))// 比较并进行设置成功
return true;
}
}
// 根据指定的缩减量减小可用许可的数目
final void reducePermits(int reductions) {
for (;;) {
int current = getState();
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}
// 获取并返回立即可用的所有许可
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))// 许可为0或者比较并设置成功
return current;
}
}
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
// 共享模式下获取
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
/**
* Fair version
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())// 同步队列中存在其他节点
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
Semaphore
自身只有两个属性,最重要的是sync
属性,基于Semaphore
对象的操作绝大多数都转移到了对sync
的操作
NonfairSync
和FairSync
都继承了Sync
类,NonfairSync
表示采用非公平策略获取资源,FairSync
表示采用公平策略获取资源,它们都重写了AQStryAcquireShared
方法,NonfairSync
会调用父类Sync
的nonfairTryAcquireShared
方法,表示按照非公平策略进行资源的获取。FairSync
使用公平策略来获取资源,它会判断同步队列中是否存在其他的等待节点。
3.2 构造函数
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
默认也是非公平锁,并发度(permits)赋值给了AQS
的volatile int state
, 并发度就是AQS的state的初始值
3.3 acquire方法
此方法从信号量获取一个(多个)许可,在提供一个许可前一直将线程阻塞,或者线程被中断,其源码如下
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
该方法中将会调用Sync
对象的acquireSharedInterruptibly
(从AQS
继承而来的方法)方法。
3.4 release方法
此方法释放一个(多个)许可,将其返回给信号量,其实是AQS
的state
加1,释放成功后,唤醒AQS
队列中等待的线程,从被阻塞的位置开始执行,源码如下。
public void release() {
sync.releaseShared(1);
}
该方法中将会调用Sync
对象的releaseShared
(从AQS继承而来的方法)方法。
3.5 小结
Semaphore
的内部工作流程也是基于AQS
,并且不同于CyclicBarrier
和ReentrantLock
,单独使用Semaphore
是不会使用到AQS
的条件队列的,其实,只有进行await
操作才会进入条件队列,其他的都是在同步队列中,只是当前线程会被park
。
3.5.1 非公平模式下的总结
- 当线程要获取许可时,可以直接调用
Semaphore
的acquire()
和tryAcquire()
方法。 - 如果调用
acquire()
方法,那么将会直接调用AQS
的acquireShared()
方法,该方法又会调用非公平同步器的tryAcquireShared()
方法,该方法会直接调用抽象同步器提供的nonfairTryAcquireShared()
方法(用当前同步资源的个数 - 要获取的同步资源个数,如果大于等于0则表示获取同步资源成功,则通过CAS更新同步状态的值,然后返回剩余的可用资源个数,否则表示获取同步资源失败,返回负数) - 如果调用
tryAcquire()
方法,那么将会直接调用抽象同步器的提供的nonfairTryAcquireShared()
方法尝试获取同步资源。 - 当线程要释放许可时,将会调用
Semaphore
的release()
方法,该方法会直接调用AQS
的releaseShared()
方法,releaseShared()
方法又会调用抽象同步器的tryReleaseShared()
方法,将当前同步资源的个数 + 要释放的同步资源个数 ,然后通过CAS
更新同步状态的值。
3.5.2 公平模式下的总结
- 当线程要获取许可时,可以直接调用
Semaphore
的acquire()
和tryAcquire()
方法。 - 如果调用
acquire()
方法,那么将会直接调用AQS
的acquireShared()
方法,该方法又会调用公平同步器的tryAcquireShared()
方法,该方法只有当当前线程是等待队列中的头节点的后继节点所封装的线程,或者当前等待队列为空或只有一个节点时,才允许尝试获取同步资源(用当前同步资源的个数 - 要获取的同步资源个数,如果大于等于0则表示获取同步资源成功,则通过CAS更新同步状态的值,然后返回剩余的可用资源个数,否则表示获取同步资源失败,返回负数) - 如果调用
tryAcquire()
方法,那么将会直接调用抽象同步器的提供的nonfairTryAcquireShared()
方法尝试获取同步资源。 - 当线程要释放许可时,将会调用
Semaphore
的release()
方法,该方法会直接调用AQS
的releaseShared()
方法,releaseShared()
方法又会调用抽象同步器的tryReleaseShared()
方法,将当前同步资源的个数 + 要释放的同步资源个数,然后通过CAS
更新同步状态的值。
四、总结
Semaphore
基于AQS
实现,分为公平和非公平模式,需要拿到许可才能执行,state
是可执行线程的数量,当获取锁时,state-1
,执行完毕时state+1
,当state
小于0时,新加入的线程需要等待直到有其他线程之心完毕释放为止。