Semaphore源码解析以及与CountDownLatch的对比

写在前面

信号量,源码中的注释是这样写的 :

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)。而释放资源的过程两者的实现都是大同小异。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章