前言
Sets head of queue, and checks if successor may be waiting in shared mode, if so propagating if either propagate > 0 or PROPAGATE status was set.
此函数被共享锁操作而使用。这个函数用来将传入参数设为队列的新节点,如果传参的后继是共享模式且现在要么 共享锁有剩余(propagate > 0) 要么 PROPAGATE状态被设置,那么调用doReleaseShared。
共享锁获取流程
比如当你调用了doAcquireShared。
private void doAcquireShared(int arg) {
final Node node = addWaiter(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(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);
}
}
- 执行doAcquireShared的当前线程想要获取到共享锁。
- addWaiter将当前线程包装成一个共享模式的node放到队尾上去。
for循环的过程分析:
- 执行到tryAcquireShared后可能有两种情况:
- 如果tryAcquireShared的返回值>=0,说明线程获取共享锁成功了,那么调用setHeadAndPropagate,然后函数即将返回。
- 如果tryAcquireShared的返回值<0,说明线程获取共享锁失败了,那么调用shouldParkAfterFailedAcquire。
- 这个shouldParkAfterFailedAcquire一般来说,得至少执行两遍才能将返回true:第一次shouldParkAfterFailedAcquirenode把前驱设置为SIGNAL状态,第二次检测到SIGNAL才返回true。
- 既然上一条说了,shouldParkAfterFailedAcquirenode一般执行两遍,那么很有可能第二遍的时候,发现自己的前驱突然变成head了并且获取共享锁成功,又或者本来第一遍的前驱就是head但第二遍获取共享锁成功了。不用觉得第一遍的SIGNAL白设置了,因为设置前驱SIGNAL本来就是为了让前驱唤醒自己的,现在自己处于醒着的状态就获得了共享锁,那就接着执行setHeadAndPropagate就好。
- 剩下的就是常见情况了。线程调用两次shouldParkAfterFailedAcquire,和一次parkAndCheckInterrupt后,便阻塞了。之后就只能等待别人unpark自己了,以后如果自己唤醒了,又会走以上这个流程。
总之,执行doAcquireShared的线程一定会是局部变量node
所代表的那个线程(即这个node的thread成员)。
setHeadAndPropagate分析
private void setHeadAndPropagate(Node node, long propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
- 入参
node
所代表的线程一定是当前执行的线程,propagate
则代表tryAcquireShared的返回值,由于有if (r >= 0)
的保证,propagate
必定为>=0,这里返回值的意思是:如果>0,说明我这次获取共享锁成功后,还有剩余共享锁可以获取;如果=0,说明我这次获取共享锁成功后,没有剩余共享锁可以获取。 Node h = head; setHead(node);
执行完这两句,h保存了旧的head,但现在head已经变成node了。h == null
和(h = head) == null
和s == null
是为了防止空指针异常发生的标准写法,但这不代表就一定会发现它们为空的情况。这里的话,h == null
和(h = head) == null
是不可能成立,因为只要执行过addWaiter,CHL队列至少也会有一个node存在的;但s == null
是可能发生的,比如node
已经是队列的最后一个节点。- 看第一个if的判断:
- 如果
propagate > 0
成立的话,说明还有剩余共享锁可以获取,那么短路后面条件。 - 中间穿插一下doReleaseShared的介绍:它不依靠参数,直接在调用中获取head,并在一定情况unparkSuccessor这个head。但注意,unpark head的后继之后,被唤醒的线程可能因为获取不到共享锁而再次阻塞(见上一章的流程分析)。
- 如果
propagate = 0
成立的话,说明没有剩余共享锁可以获取了,按理说不需要唤醒后继的。也就是说,很多情况下,调用doReleaseShared,会造成acquire thread不必要的唤醒。之所以说不必要,是因为唤醒后因为没有共享锁可以获取而再次阻塞了。 - 继续看,如果
propagate > 0
不成立,而h.waitStatus < 0
成立。这说明旧head的status<0。但如果你看doReleaseShared的逻辑,会发现在unparkSuccessor之前就会CAS设置head的status为0的,在unparkSuccessor也会进行一次CAS尝试,因为head的status为0代表一种中间状态(head的后继代表的线程已经唤醒,但它还没有做完工作),或者代表head是tail。而这里旧head的status<0,只能是由于doReleaseShared里的compareAndSetWaitStatus(h, 0, Node.PROPAGATE)
的操作,而且由于当前执行setHeadAndPropagate的线程只会在最后一句才执行doReleaseShared,所以出现这种情况,一定是因为有另一个线程在调用doReleaseShared才能造成,而这很可能是因为在中间状态时,又有人释放了共享锁。propagate == 0
只能代表当时tryAcquireShared后没有共享锁剩余,但之后的时刻很可能又有共享锁释放出来了。
- 继续看,如果
propagate > 0
不成立,且h.waitStatus < 0
不成立,而第二个h.waitStatus < 0
成立。注意,第二个h.waitStatus < 0
里的h是新head(很可能就是入参node)。第一个h.waitStatus < 0
不成立很正常,因为它一般为0(考虑别的线程可能不会那么碰巧读到一个中间状态)。第二个h.waitStatus < 0
成立也很正常,因为只要新head不是队尾,那么新head的status肯定是SIGNAL。所以这种情况只会造成不必要的唤醒。
- 如果
- 看第二个if的判断:
s == null
完全可能成立,当node是队尾时。此时会调用doReleaseShared,但doReleaseShared里会检测队列中是否存在两个node。- 当
s != null
且s.isShared()
,也会调用doReleaseShared。
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.
源码注释自己也说了,if判断这么写是可能造成不必要的唤醒的。
总结
setHeadAndPropagate
函数用来设置新head,并在一定情况下调用doReleaseShared
。- 调用
doReleaseShared
时,可能会造成acquire thread不必要的唤醒。个人认为,作者这么写,是为了防止一些未知的bug,毕竟当一个线程刚获得共享锁后,它的后继很可能也能获取。 - 可以猜想,
doReleaseShared
的实现必须是无伤大雅的,因为有时调用它是没有必要的。 - PROPAGATE状态存在的意义是它的符号和SIGNAL相同,都是负数,所以能用
< 0
检测到。因为线程刚被唤醒,但还没设置新head前,当前head的status是0,所以把0变成PROPAGATE,好让被唤醒线程可以检测到。