写在前面
信号量,源码中的注释是这样写的 :
A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each {@link #acquire} blocks if necessary until a permit is available, and then takes it. Each {@link #release} adds a permit, potentially releasing a blocking acquirer.
其大致的意思是:它是一个计数信号量。概念上它是一个持有很多“许可”的信号量。每一个acquire方法都会被阻塞,直到有可用的许可,而每一个release方法都将产生一个“许可”,进而释放掉阻塞的acquire.
总体上,semaphore可以表达成以下这样的形状:
上图为信号量的模型,许可池就是信号量对象初始时设置的“许可”数量,然后每个线程要想做事情,就需要先获取“许可”,如果获取到许可,池里面的“许可”就减一,如果事情做完了,则释放许可(放回),没有获取到许可的线程则需要等待。从以上内容我们可以看到其最重要的两个方法就是acquire和release,接下来我们看一下重点的代码实现。
总体结构
abstract static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) { // 初始化许可池
setState(permits);
}
final int getPermits() {
return getState();
}
//非公平策略的tryAcquireShared
final int nonfairTryAcquireShared(int acquires) {
for (;;) { //自旋+cas
int available = getState(); //获取当前状态,也就是可用的许可
int remaining = available - acquires; //减去本次获取的许可
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining; //如果剩余许可小于0,则返回复数表示获取共享锁失败,否则通过cas改变剩余许可
}
}
//释放共享锁
protected final boolean tryReleaseShared(int releases) {
for (;;) {//自旋+cas
int current = getState();//获取当前状态,也就是可用的许可
int next = current + releases; //加上本次释放的许可
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next)) //同样通过cas改变剩余许可
return true;
}
}
}
//非公平策略的同步器实现
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);
}
}
//公平策略的同步器实现
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;
}
}
}
以上代码我只贴了nonfairTryAcquireShared和tryReleaseShared,其它方法类似,这里主要是想说明,semaphore利用AQS做了什么。
从构造方法来看,这里会初始化state,给到一个初始化许可池(这里的实现与CountDownLatch非常相似,详情见:CountDownLatch源码解析,都是先初始state为一个整整数,然后后续的操作都是围绕这个state来进行。
核心方法
首先看acquire方法:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
这里可以看出,acquire其实就是调用了同步器的acquireSharedInterruptibly方法,而这个方法的实现是通过子类对具体的tryAcquireShared的实现来体现差异的,这里自然就联想到了我们这里的tryAcquireShared方法实现。逻辑非常简单,就是从当前的state减去本次获取的值,然后cas更新state值,如果减去后新的state小于0,则代表获取共享资源失败,则当前线程会不但尝试获取或者睡眠(见AQS实现)。
同理,release也是同样类似的逻辑:
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
释放锁调用了releaseShared方法,而该方法最后会调用这里的实现tryReleaseShared方法,这里的tryReleaseShared方法同样比较简单,就是正常的累加许可,这里不再累述。
总结
其实看semaphore的代码更应该结合CountDownLatch的代码来看,这两个线程协调器的实现非常类似,以下对比以下他们的异同点。
1.CountDownLatch是让一组线程等待在某个点,然后外界达到某种条件后则这组线程会被通过放行。Semaphore则是线程在许可池中尝试获取许可,获取成功则直接通过,获取不到则等待。所以Semaphore的目标不是让线程协同,而是对运行的线程进行限量。
2.在实现上它们两是非常相似的,它们都是通过自己实现AQS来达到目的,都是首先初始化state为某个值。不同点在于各自对tryAcquireShared和tryReleaseShared的实现,对于CountDownLatch来说,只要state不为0则代表外界条件还没满足,所以获取共享资源统一返回失败,从而实现了线程的等待,而对于Semaphore来说,是要切实的去判断许可池是否还有可用的许可(即state减去本次获取的许可后是否大于0)。而释放资源的过程两者的实现都是大同小异。