阅读本篇文章基础前提是对AQS有一定认识,如果看过前面的ReentranLock和CountDownLatch源码分析,看这篇文章就会轻松无比,且本文仅对最基本的acquire()方法和release()方法进行源码分析,其他版本获取、释放信号方法实现原理基本一致。
概述
Semaphore的概念类比操作系统的信号量,当然本身人家就叫semaphore。可以理解成Semaphore代表一组资源个数,使用的时候要调用acquire()方法来获取,释放时调用release()方法。资源被获取完了后,调用acquire()方法会阻塞至此方法调用处。
下面抄一段《java并发编程实战》里面的使用实例,看一下用法。
下面这段代码实现了一个有界的HashSet,如果添加元素超过容量会被阻塞,直到容器中有元素被取出而空出了位置。
public class BoundedHashSet<T> {
private final Set<T> set;
private final Semaphore sem;
public BoundedHashSet(int bound){
set = Collections.synchronizedSet(new HashSet<T>());
sem = new Semaphore(bound);
}
public boolean add(T o) throws InterruptedException {
sem.acquire(); //每添加一个元素,acquire()一次获取一个信号,信号取完后会阻塞至此,直到调用Semaphore.release()释放一个信号
boolean wasAdded = false;
try {
wasAdded = set.add(o);
return wasAdded;
}finally {
if (!wasAdded)
sem.release();
}
}
public boolean remove(Object o){
boolean wasRemoved = set.remove(o);
if(wasRemoved)
sem.release(); //每取出一个元素,release()一次释放一个信号
return wasRemoved;
}
}
Semaphore和concurrent包里的ReentranLock、CountDownLatch等都是以AQS为基础来实现的。使用内部类Sync继承AQS抽象类,并且提供继承Sync的FairSync和NonfairSync供实例化Semaphore时选择使用公平方式还是非公平方式获取信号。Semaphore和CountDownLatch一样也是使用AQS的共享模式
成员
1、Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 1192457210091910933L;
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
/**
* 尝试以非公平方式获取acquires个信号,非公平方式即不管等待队列中等待已久的其他线程,直接尝试使用CAS获取,获取失败会被加入等待队列
* 方法写到一个死循环中,只有获取成功或者"当前信号数 < 本次需要获取信号数"跳出循环
* 获取成功返回正值,失败返回负值
* <p>方法除了cas外没有用到任何同步的操作,原因在与for(;;)的骚操作,获取当前state值与cas都包在这个死循环
* 代码块里,如果本线程由于其他线程竞争关系导致cas失败,会再次重新执行在当前的state值减去acquire后执行cas
* </p>
*/
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
/**
* 尝试释放release个信号
* 释放成功返回true,溢出抛Error
* 方法除了cas外没有用到任何同步的操作,原因在与for(;;)的骚操作,获取当前state值与cas都包在这个死循环
* 代码块里,如果本线程由于其他线程竞争关系导致cas失败,会再次重新执行在当前的state值减去acquire后执行cas
*/
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow 溢出抛Error
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
/**
* 用于后续调整减少信号量
* @param reductions 减少的个数
*/
final void reducePermits(int reductions) {
for (;;) {
int current = getState();
int next = current - reductions;
if (next > current) // underflow 溢出抛Error
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))
return;
}
}
/**
* 将信号量置0
* @return 0
*/
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
}
2、NonfairSync和FairSync
真正使用的NonfairSync和FairSync实现如下:
/**
* 非公平版本
*/
static final class NonfairSync extends Semaphore.Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
/**
* <p>非公平方式获取信号,非公平方式即不管等待队列中等待已久的其他线程,直
* 接尝试使用CAS获取,获取失败会被加入等待队列</p>
* 实例化AQS中对应方法,直接调用的Sync的nonfairTryAcquireShared()方法
* @param acquires 获取信号量个数
* @return 成功返回正值(当前总信号量),失败返回一个负值
*/
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
/**
* 公平版本
*/
static final class FairSync extends Semaphore.Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
/**
* 公平方式获取信号,如果等待队列中有线程在等待信号,直接返回负值表示失败
* 实例化AQS中对应方法
* @param acquires 获取信号量个数
* @return 成功返回正值(当前总信号量),失败返回一个负值
*/
protected int tryAcquireShared(int acquires) {
for (;;) {
//如果等待队列中有线程在等待信号,直接返回负值表示失败,自己会被加入等待队列
if (hasQueuedPredecessors())
return -1;
//等待队列中没有等待信号的线程,大家公平竞争,谁cas成功谁获取信号,失败的下次循环重新获取
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || //信号量此时不能满足需求,返回负值表示失败,自己会被加入等待队列
compareAndSetState(available, remaining))
return remaining;
}
}
}
源码分析
构造函数
提供两个版本构造函数,一个只传入初始信号量大小,另一个传入初始信号量大小和所选Semaphore模式为公平或非公平的boolean值来创建Semaphore。
只传入初始信号量大小的版本默认使用非公平模式,非公平模式可以从宏观上提供更高的效率,能一定程度避免了线程的阻塞唤醒代价。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
传入初始信号量大小和所选Semaphore模式为公平或非公平。可以选择使用公平模式还是非公平模式来初始化Semaphore
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
获取信号量
1、acquire()方法
从源码可以看到直接调用父类AQS实现的acquireSharedInterruptibly(1)方法,共享模式下获取一个信号。
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
acquireSharedInterruptibly()方法主要步骤为:
- 判断本线程是否已经被中断,如果是直接抛中断异常
- 尝试调用tryAcquireShared()方法获取信号
- 尝试获取失败后,调用doAcquireSharedInterruptibly()方法获取信号
实现如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) //FairSync或NofairSync实例化的tryAcquireShared()方法,尝试获取arg个信号,请回头看前文实现分析
doAcquireSharedInterruptibly(arg);//尝试失败后调用该方法获取arg个信号
}
doAcquireSharedInterruptibly()方法我们在CountDownLatch源码分析中做过详细解释,这里直接抄过来。这个方法处理步骤描述如下:
- 为当前线程创建一个共享模式节点链接到等待链表尾部
- 自旋的等待获取锁,自旋到一定条件会被阻塞,被阻塞后就等待被释放信号的线程唤醒重新自旋的获取锁。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将当前线程以共享模式创建一个等待节点,并加入等待队列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true; // 用于记录是否在调用tryAcquireShared()方法是否抛出异常
try {
for (;;) {
/**
* 自旋的等待当前线程等待节点的前序节点为head节点;结束自旋的方式有两个,1、当前节点前驱节点就是head,2、调用LockSupport.park()中断循环,等待被unpark()唤醒再次进入循环
* 如果当前等待线程节点前序节点为head节点时,使用tryAcquireShared()方法尝试获取锁,
* 获取成功将当前线程等待节点设置为head节点,并且如果后继节点为共享状态节点,则唤醒它们,让它们自旋的等待获取锁
*/
final Node p = node.predecessor();
// 如果当前线程等待节点前驱节点为head节点
if (p == head) {
int r = tryAcquireShared(arg); // 尝试获取锁
if (r >= 0) { // 获取成功
setHeadAndPropagate(node, r); // 将当前线程等待节点设置为head节点,并且如果后继节点为共享状态节点,则唤醒它们,让它们自旋的等待获取锁
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && // 判断当前线程是否应该被park()中断于此
parkAndCheckInterrupt()) // 调用LockSupport.park(this)阻塞当前获取锁的线程,并且调用Thread.interrupted()返回线程是否被中断了
throw new InterruptedException(); // 如果线程被中断了抛出InterruptedException异常
}
} finally {
// 如果AQS子类实现的tryAcquireShared()方法抛异常了,failed不会被置false,这时就执行cancelAcquire方法清理现场
if (failed)
cancelAcquire(node);
}
}
2、释放信号方法
使用release()方法来释放一个信号,方法就一行,就是直接调用AQS实现的releaseShared(1)方法来释放一个信号。
public void release() {
sync.releaseShared(1);
}
releaseShared()方法在CountDownLatch源码分析中也做了详细解释,这里直接抄过来。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 调用FairSyn或NofairSync重写的tryReleaseShared()方法尝试把代表信号的state减1, state通过减1成功后需要执行doReleaseShared()方法(即表达信号刚刚被当前线程释放了)
doReleaseShared(); // 传递的唤醒后面共享模式等待节点,让它们接着在doAcquireSharedInterruptibly()方法自旋的获取信号
return true;
}
return false;
}
tryReleaseShared(int releases) 方法实现请查看上文中FairSync和NofairSync中的实现。