在之前分析的核心方法都是排他锁,今天开始聊聊共享锁的获取与释放。
一、获取锁acquireShared
acquireShared
方法是获取共享锁的方法:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
入口只包含两个子方法:
tryAcquireShared
是获取【锁】的方法;doAcquireShared
是加入同步队列的方法;
线程一开始先获取锁,如果成功,则不进入队列中,如果失败,才进入同步队列进行管理。
这里其实我有个疑问,后续我们会在doAcquireShared
方法里看到,当前线程获得所之后,如果共享锁还有余额(比如CountDownLatch的count值,或者Semphore的信号量),就会从队列的头部开始唤醒线程,直到共享资源用光,但是这里只判断了tryAcquireShared(arg) < 0
,但是如果tryAcquireShared(arg) > 0
,完全可以执行一次唤醒的过程。
进入doAcquireShared
方法:
private void doAcquireShared(long arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
long 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);
}
}
这个方法乍一看和之前的addQueue方法类似,其实不同的就在于自旋过程中获取到锁之后,执行setHeadAndPropagate
方法,这个先不急,我们先对方法梳理一下:
其中shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
方法已经在AQS核心流程解析-acquire方法文中解释过了,这里就不重复了。
重点需要分析下setHeadAndPropagate
,执行到这里说明当前线程成功获取到锁资源,之所以叫共享锁,就是不仅仅"我"这个节点可以获取锁,其他节点也能获取到,那么就要给出一个"通知",叫醒其他兄弟,起来拿锁了。先看看代码:
private void setHeadAndPropagate(Node node, long propagate) {
Node h = head;
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();
}
}
这个方法拆成三部分看:
setHead(node);
当前节点获取到锁,然后把自己变成头节点,这样移动head指针,后面一个再获取到的锁的时候,"我"就会被垃圾回收了;- if的判断条件
doReleaseShared
方法:这个方法虽然叫释放锁,但是实际上唤醒队列的等待节点,吼一声"兄弟们,开饭了,快冲啊",而真正释放锁资源的逻辑在releaseShared
的tryReleaseShared
,所以感觉这个方法叫notifyWaiters或者propagateQueue比较好。
第1点之前说过,第3点待会讲,这里我们分析下if判断条件:
propagate > 0
,如果共享资源还有富裕的时候,这个时候可以大胆的让兄弟们拿锁;- 即使不富裕,比如
propagate = 0
?这个时候,锁资源也有被其他线程释放的可能性,那么也考虑唤醒线程,大不了抢也抢不到,那就继续等着呗;其中h == null
应该是防止空指针的情况,h.waitStatus < 0
则是需要头节点是SINGAL或者PROPAGATE状态,这两个状态都表示后继节点没有获取到锁,所以需要通知队列的后续进程起来拿锁。
那么什么情况,不会执行到if语句呢?
propagate == 0 (至于为什么不会小于0,请看看进入setHeadAndPropagate的条件),并且h.waitStatus >= 0,这是什么意思呢,就是锁资源不够了并且后续节点取消了或者正在运行中,不需要"我"来唤醒。
这里再顺便提一句,后续节点在取消的时候,也会环境他的后继。
进入到第一层的if之后,还会遇到if (s == null || s.isShared())
,这个就是说,"我"的下一个节点是空或者后继等待的是共享锁:
- 如果"我"的下一个节点是空的说明什么呢?要么是"我"就是尾节点,要么"我"是取消节点,当然这只是局部地猜测可能性,但是从我们现在所处的流程,应该不会出现这些情况
s.isShared()
如果s非空,就后继必须是共享模式,你想想啊,后继如果是排他模式,我自己又是共享模式(而且目前我还是头节点),那没必要从头再唤醒了,因为后继想要拿的是一把排他锁,必须等到别的独占线程释放才行。
过五关斩六将,我们到了最后一关,就是doReleaseShared
,刚才也提到,这个方法实际上唤醒队列的等待节点,我们先看下代码:
private void doReleaseShared() {
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;
}
}
这个方法逻辑可以用总结如下:
如果后继已经阻塞了,就唤醒他;
如果后继还正在运行中,则标记为PROPAGATE,这样后继在判断到前置节点的状态为PROPAGATE的时候,不会阻塞(shouldParkAfterFailedAcquire
),而是继续自旋;
然后如果唤醒的操作完成后,发现头节点没变,说明后续节点还未拿到锁,那就让后继节点继续努力吧,"我"的接力棒已经交出去了,可以收工了,所以return就行了。
至于代码中continue;
部分,其实就是为了防止在"瞬息万变"的线程世界中,后继和"我"交替运行,一旦CAS失败,说明后继节点已经修改过,那么就赶紧自旋,然后从新的头节点开始,直到成功为止。
这里还可以看出,获取到共享锁之后,还要自旋着通知后继线程,这"共享精神"值得钦佩哦。
二、释放锁releaseShared
为什么这次释放锁和获取锁合并在一起讲呢,你看看释放锁的代码:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
核心的doReleaseShared
已经讲过啦,所以the end…