Semaphore简介
无论是内部锁synchroized还是RentrantLock,同一时刻只允许一个线程访问资源,而信号量(Semaphore)却可以指定多个线程同时访问一个资源。Semaphore相当于共享锁版的RentrantLock。
主要流程为:
- 创建一定数量的许可
- 不断的消耗许可,当许可被消耗完毕,后面的线程进行队列排队,设置头节点
- 当有许可被释放的时候,唤醒节点进行许可申请。重置 state 许可数量
# 使用方式
Semaphore semaphore = new Semaphore(5);
semaphore.acquire();
semaphore.release();
# 方法解释
java public Semaphore(int permits)
:创建具有给定的许可数和非公平的公平设置的 Semaphore,公平指的是排队阻塞的线程先进先出的规则。
javapublic Semaphore(int permits, boolean fair)
创建具有给定的许可数和给定的公平设置的 Semaphore。
第一个参数是信号量的准入数,即同事能申请多少个许可,当每个线程只申请一个许可时,就那相当于指定了有多少个线程可以同事访问某个资源。
acquire()
尝试获得一个准入的许可,若无法获得,则线程会等待,直到有线程释放许可或者当前线程被中断。
release()
释放一个许可,将其返回给信号量。
# 一个Demo
public class SemapDemo implements Runnable {
final static Semaphore semp = new Semaphore(5);
public void run(){
try {
semp.acquire();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" done");;
semp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String args[]) {
ExecutorService exec = Executors.newFixedThreadPool(20);
final SemapDemo demo = new SemapDemo();
for(int i=0;i<20;i++){
exec.submit(demo);
}
}
}
原理分析
Semaphore类的结构图如下,其中红色的线表示内部类,蓝色的线表示继承。
根据构造方法的参数,来构建内部公平或者非公平的Sync实例,赋值给成员变量Sync,后面的acquire和release方法都是委托给sync来操作。
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
FairSync(int permits) {
super(permits);
}
Sync(int permits) {
setState(permits);
}
根据构造函数的调用链,可以看出permits最终赋值于AQS的state属性。这里和CountDownLatch颇为相似。state在CountDownLatch终表示计数器的值,即几个线程可以去调用countDown方法,而在Semaphore中state表示许可的数量。
# acquire
acquire的调用流程为:Semaphore.acquire
-> AQS.acquireSharedInterruptibly
-> NonfairSync/FairSync.tryAcquireShared
-> AQS.doAcquireSharedInterruptibly
tryAcquireShared根据不同的公平锁和非公平锁的实现,作用是尝试获取锁,如果没有获取到锁,才回去执行doAcquireSharedInterruptibly,doAcquireSharedInterruptibly的作用是入队、尝试、挂起线程。
我们直接进入AQS的acquireSharedInterruptibly方法,在分析CountDownLatch中也讲过该方法。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
如果获取许可的结果>=0,表示获取成功。否则,进入doAcquireSharedInterruptibly方法。
// 非公平锁的释放锁的方法
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
// 获取许可的数量
int available = getState();
// 计算剩余数量
int remaining = available - acquires;
// 如果小于 0,直接返回remaining,表示获取锁失败,如果大于0,CAS设置。
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
接着doAcquireSharedInterruptibly方法也是在CountDownLatch中分析过,此处就不贴代码了,其大体逻辑为:
- 创建一个共享型的节点添加至同步队列的尾部
- 如果这个节点的上一个节点是 head ,就是尝试获取锁,获取锁的方法就是子类重写的方法。如果获取成功了,就将刚刚的那个节点设置成 head。
- 如果前一个节点不是head,或者获取锁失败,则去挂起线程
#release
release过程和CountDownLatch的countDown逻辑基本一样,通过AQS的releaseShared方法内部调用子类实现的tryReleaseShared法方法,表示释放许可,接着唤醒阻塞的线程。这里有个点需要
注意下即使在某个线程中没有调用acquire方法,也可以直接使用release,相当于增加许可的数量。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 唤醒 AQS 等待对列中的节点,从 head 开始
doReleaseShared();
return true;
}
return false;
}
// Sync extends AbstractQueuedSynchronizer
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < curre
if (compareAndSetState(current, next))
return true;
}
}
逻辑很简单,唤醒线程方法doReleaseShared在CountDownLatch中分析过,此处不再赘述。