AQS共享模式
本文对上一章中,共享模式做一个补充。上一章提到,AQS最开始的版本,是没有PROPAGATE状态的,有BUG,本文解释一下,这个BUG是怎么回事儿,是如何通过PROPAGATE状态解决这个BUG的。 参考:
1. 获取共享锁
来看下共享版本的acquireQueued源码:
/**
* 调用子类的tryAcquireShared方法,失败了,调用doAcquireShared,尝试入队等待
*/
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
/**
* 共享版本的acquireQueued
*/
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);//模式为Node.SHARED(共享)
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//重点区别,获取锁成功后,调用的是setHeadAndPropagate,而非setHead
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
简单过下代码,与独占模式的acquireQueued区别不多:
- 入队列时,节点的状态为Node.SHARED模式,这点没什么好解释的。
- 获取锁成功后,调用的是setHeadAndPropagate,而非setHead。这是核心区别,独占模式下,获取锁成功后,退出队列即可,因为不可能有其他线程,能够成功获取锁了;共享模式下不同,还可以,因此需要向后传播(Propagate)。
接下来,看下setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 注意这里都是或,也就是说,即使propagate<=0,
// 只要head(不管是旧的还是最新的)的状态<0,就会进入下面代码块
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// next = null,无法获取通过next获取下一节点,保守策略,执行doReleaseShared
// 或者下个节点是共享模式,执行doReleaseShared
if (s == null || s.isShared())
doReleaseShared();
}
}
上面代码中,(propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0)这个条件判断,十分让人疑惑,注意这里都是或,也就是下列条件任意一个成立,就会尝试唤醒后续节点
propagate > 0,propagate大于0,也就是执行tryAcquireShared方法成功后还有剩余资源;h == null || h.waitStatus < 0,在执行setHead方法前的head,状态小于0(我也不知道h==null是啥场景);(h = head) == null || h.waitStatus < 0),在执行setHead方法后的head,状态小于0(由于可能存在并发,其实这个head也不一定是参数中的Node节点)。
propagate大于0,还有剩余资源,执行doReleaseShared唤醒队列中的线程,可以理解,为什么即使剩余资源等于0,在特定条件下,也会执行呢?
其实,在在1.6较早版本中,只有propagate大于0,才会尝试唤醒后继节点,是有缺陷的:
bug: bugs.openjdk.java.net/browse/JDK-… fix: github.com/openjdk/jdk… 在fix的链接中,可以看到修改的细节。我们这里看下这个bug是如何发生的。
2. bug详情
先看下之前有bug的源码,setHeadAndPropagate方法,唤醒后继的条件很简单,就是有剩余资源且当前节点状态不为0(也就是SIGNAL)。
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
if (s == null || s.isShared())
unparkSuccessor(node);
}
}
而释放锁的方法,也很简单:
public final boolean releaseShared(long arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
在这个版本中,共享模式资源的release,与独占模式的release,除调用的释放资源的方法不同外,没有什么区别。下面解释一下,bug触发的场景,具体代码在上面的链接中有,使用Semaphore触发的。
- 我们假设现在有两个线程,以共享的模式,在队列中等待:
- 某个线程释放了一个资源
- 执行unparkSuccessor方法,把head节点waitStatus改为0,并唤醒T1节点。
- T1节点被唤醒后,调用tryAcquireShared方法,返回0,获取资源成功(state=0),此时剩余资源为0,但还没有出队列。
- 某持有资源的线程又释放了一个资源(state=1),判断head的waitStatus为0,不会唤醒后继节点。
- T1线程调用setHeadAndPropagate方法出队列,T1节点变为哨兵节点,但因为之前调用tryAcquireShared方法时返回0,导致执行setHeadAndPropagate方法时,不会唤醒后继节点。
这就导致本应被唤醒的T2,无法被唤醒。根本原因是,setHeadAndPropagate方法中,判断是否需要唤醒后继节点时,使用的是之前执行tryAcquireShared方法,那个时刻的propagate状态,在判断状态时,可能已经不准确了(可能更多也可能更少了)。如果tryAcquireShared返回的propagate=0,即使其他线程归还了资源,该线程(例子中的T1)线程,是无法感知到的,也就无法唤醒后继节点。其实上面栗子中的第二步,哨兵的节点状态,并不十分重要,即使是SIGNAL,也可能后续还是唤醒了T1这个已经活跃的线程。
我们再举一个跟上面类似的栗子:
- 初始状态时,也是两个节点,但是T1没有阻塞,T2阻塞了
- 线程T3持有资源的线程释放了一个资源(state=1),还没有执行unparkSuccessor方法,因为T1还没有阻塞,直接获取到了该资源(state=0),执行到setHeadAndPropagate的setHead之前
- 线程T4又释放了一个资源(state=1),线程T3/T4都判断,waitStatus = 1,调用unparkSuccessor方法,唤醒T1,当然没什么效果,因为T1本来就是活跃状态。
- T1线程调用setHeadAndPropagate方法出队列,T1节点变为哨兵节点,但因为之前调用tryAcquireShared方法时返回0,导致执行setHeadAndPropagate方法时,不会唤醒后继节点。
❄️ 可不可以在setHeadAndPropagate直接读state的状态呢?
不可以,理论上AQS并没有规定state的含义,没有规定state代表资源的数量,state的含义,完全由子类定义,因此不应该直接使用state状态作为资源。
3 PROPAGATE(-3)状态
上面的栗子中,由于线程T1在setHeadAndPropagate方法中,判断剩余资源时,无法获取最新的资源数量,即使有新的线程归还了资源,T1也无法得知。
李狗哥,通过新增PROPAGATE状态,来解决这个bug。当线程归还资源后,如果head节点状态为0,就改为PROPAGATE;而T1线程判断head的状态,如果为PROPAGATE,则不管剩余资源是多少,都尝试释放资源。
先看下最新的释放共享锁源码:
/**
* 释放共享锁,核心封装在doReleaseShared中。
*/
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
/**
* Release action for shared mode -- signals successor and ensures
* propagation. (Note: For exclusive mode, release just amounts
* to calling unparkSuccessor of head if it needs signal.)
*/
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;
// 状态为SIGNAL,唤醒后继节点
if (ws == Node.SIGNAL) {
// CAS失败重新循环(head不变,且状态不变,下次循环进入else,会把状态改为PROPAGATE)
// 一个线程unparkSuccessor就可以,毕竟多个线程同时unpark同一个线程,也没什么用
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 状态==0,则CAS改为PROPAGATE,告知其他在执行setHeadAndPropagate的线程,有新资源释放,无条件唤醒
// CAS失败,continue重新循环
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果头结点改变了,继续循环,防止head的状态被改为0
if (h == head) // loop if head changed
break;
}
}
重点解释下doReleaseShared方法,这个doReleaseShared会并发,可能是多个线程释放资源,也可能是队列中的节点获取锁成功,也在调用doReleaseShared方法,就像上文中bug复现的场景那样。
- head状态为Node.SIGNAL,CAS改状态为0,争抢执行unparkSuccessor机会。CAS失败,说明存在并发,continue重新循环,如果head和head的状态没有改变,则进入下面的else,尝试修改状态为PROPAGATE(避免例子2)。
- head状态为0,则CAS改状态为PROPAGATE,告知其他在执行setHeadAndPropagate的线程,有新资源释放,无条件唤醒。CAS失败continue重新循环:
- 这段代码并发修改失败了,重试确认下就好;
- 与取消并发了,取消会把它的前置节点的状态,从0改为SIGNAL。
- 循环结尾,如果头节点没变,才会退出,也就会说,如果head节点改变,会重新判断。
简单概括下,doReleaseShared,如果head节点为SIGNAL,就执行unparkSuccessor,如果是0,就改为PROPAGATE,告知其他在执行setHeadAndPropagate的线程,有新资源释放,无条件唤醒。
现在回过头来,看下最新的setHeadAndPropagate方法:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
// 注意这里都是或,也就是说,即使propagate<=0,
// 只要head(不管是旧的还是最新的)的状态<0,就会进入下面代码块
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// next = null,无法获取通过next获取下一节点,保守策略,执行doReleaseShared
// 或者下个节点是共享模式,执行doReleaseShared
if (s == null || s.isShared())
doReleaseShared();
}
}
这回就没那么难理解了
```java
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0){
...
}
这段代码就是说,即使剩余资源小于0,在头结点(无论在setHead之前,还是之后的),如果状态小于0,就执行doReleaseShared方法。
❄️ 为什么不判断h.waitStatus == PROPAGATE,而是小于0呢
因为PROPAGATE状态也可能被改为SIGNAL,节点取消,就有这种逻辑。