AQS是concurrent包的基础和核心,但的确很难理解,因为用的模板方法模式,所以它要适配各种并发工具,看源码的时候里面就会产生很多让人百思不得其解的地方。再加上,独特的代码风格,阅读起来的确很费劲。今天尝试由Semaphore切入,来理解下AQS相关的设计。用Semaphore来切入的原因是Semaphore代码比较简单一些,主要就一个acquire方法和一个release方法,用来理解代码设计会轻松一些。
Semaphore介绍
源码上的注释说的是,Semaphore 从概念上讲维护了一组permits(许可)。在能获得许可之前,所有尝试执行acquire方法的线程将被阻塞。每一次执行relase方法后,都将增加一个许可,并有可能唤醒被阻塞住的线程。Semaphore常被用来限制尝试去获取资源的并发访问的线程数。
简单来说,就是给并发设定一定数量的许可,许可量归0的时候则阻塞后续的线程,直到有线程释放许可。
示例可以参考https://blog.csdn.net/huyaowei789/article/details/106690603
源码分析
先看acquire方法
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
重点是tryAcquireShared方法,这是一个AQS的钩子方法,是子类必须实现的方法。此方法需要查询当前同步状态并判断同步状态是否符合预期,然后再CAS设置同步状态。Semaphore这里有两种实现方式,一种是公平锁的方式,一种是非公平锁的方式:
//公平锁
protected int tryAcquireShared(int acquires) {
for (;;) {
//hasQueuedPredecessors方法判断当前线程是否在同步队列队首,若不是直接返回-1,执行后续进入同步队列的逻辑。
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
//非公平锁
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
这个方法比较简单,就是取出当前剩余的许可量,然后减去要尝试获取的许可量,如果计算值大于等于0则通过cas设置新的许可量剩余值,若计算值小于0。则执行后续方法doAcquireSharedInterruptibly,当前线程可能就会进入同步队列等待。
doAcquireSharedInterruptibly方法是AQS中定义的一个模板方法。作用就是在共享可中断模式下获取许可,失败则进入同步队列等待
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//判断是否是队首节点
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
addWriter方法是通过cas的方式,将当前节点插入同步队列队尾,失败则执行eng方法不断重试。然后判断当前节点是否是队首节点,若是则再次尝试尝试获取许可,失败则重试,成功则执行setHeadAndPropagate方法,将当前节点设为头节点,以便让当前节点出队,再执行doReleaseShared方法尝试唤醒后续节点(这里注意当前节点的调用方就是当前线程,所以不用唤醒当前节点。这里容易搞糊涂了)。若不是,则会先执行shouldParkAfterFailedAcquire方法判断节点状态是否是应该被阻塞。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
shouldParkAfterFailedAcquire会判断前继节点状态,若是Signal则会直接返回true,然后执行parkAndCheckInterrupt方法阻塞当前线程。若不是,且状态大于0(大于0则代表当前是取消状态)则会跳过前继节点之前所有状态是取消的节点。若前两者都不是,则会将前继节点状态设为Signal,然后由外层方法循环重试,以保证在阻塞前当前线程是无法获取到许可的(真麻烦,甚至感觉还有点啰嗦,但这也是严谨的体现)。
接下来是release方法:
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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;
}
}
原理相同,tryReleaseShared同样是个钩子方法,由Semaphore实现,会将当前剩余许可量加上你需要释放的许可量。若计算值小于还小于当前值,则直接报错。否则通过CAS重试设置当前状态为最新值。
然后是模板方法doReleaseShared:
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
这个方法的主要作用就是去唤醒同步队列中等待的节点(前面获取步骤中也调用过此方法哦)。首先若头节点的状态是Signal,则更新头节点状态并唤醒后续节点。若状态为0(初始状态)则尝试更新头节点状态为PROPAGATE?这里不太明白,注释说的是确保释放后继续传播,可能这个地方更其他同步工具的使用有关系,以后需要慢慢分析。最后如果头节点变动了,说明有并发竞争发生,则重新循环以上步骤。
以上就是对Semaphore源码的分析,Semaphore应该算是并发包下很简单的同步工具了,也推荐大家把这个类当作入口来学习AQS。