上篇我们讲了Java的AQS详解1--独占锁的获取及释放,本篇接着讲共享锁的获取及释放。
加锁
共享锁加锁的方法入口为:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared(arg)尝试获取锁,由AQS的继承类实现。
若返回值为负,证明获取锁失败,紧接着执行doAcquireShared(arg)方法。
doAcquireShared
private void doAcquireShared(int arg) {
// 将该线程封装成共享节点,并追加到同步队列中
final Node node = addWaiter(Node.SHARED);
// 失败标志
boolean failed = true;
try {
// 中断标志
boolean interrupted = false;
for (;;) {
// 获取node的前继节点
final Node p = node.predecessor();
// 若node的前继节点为head节点,则执行tryAcquireShared方法尝试获取锁(资源)
if (p == head) {
int r = tryAcquireShared(arg);
// 若返回值>=0,表明获取锁成功
if (r >= 0) {
// 将当前节点设置为head节点,并唤醒后继节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 如果中断标志位true,响应掉中断
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 若前继节点不为head节点或者前继节点为head,但tryAcquireShared获取锁失败
// shouldParkAfterFailedAcquire自旋CAS将node的前继节点的状态设置为SIGNAL(-1),并返回true
// parkAndCheckInterrupt将线程阻塞挂起,重新被唤醒后检查阻塞期间是否被中断过,将interrupted置为true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 若线程异常,则放弃获取锁
if (failed)
cancelAcquire(node);
}
}
可以看到,doAcquireShared方法和独占锁的acquireQueued方法逻辑类似,主要有2点不同:
- doAcquireShared方法直接将中断响应掉了,而acquireQueued只是返回中断标志,是否响应留在了acquire方法中;
- doAcquireShared方法获取锁成功之后,除了将当前节点设置为head之外,还有个唤醒后继节点的操作,即setHeadAndPropagate方法。
setHeadAndPropagate
private void setHeadAndPropagate(Node node, int propagate) {
// 原有head节点备份
Node h = head;
// 将当前节点设置为head
setHead(node);
// 若propagate>0(有剩余资源)或者原head节点为null或原head节点的状态值<0
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 获取node的后继节点
Node s = node.next;
// 若后继节点为null或者为共享节点,则执行doReleaseShared方法继续传递唤醒操作
if (s == null || s.isShared())
doReleaseShared();
}
}
doReleaseShared
private void doReleaseShared() {
for (;;) {
// 此时的head节点已经被替换为node节点了
Node h = head;
// 若head不为null且不是tail节点
if (h != null && h != tail) {
int ws = h.waitStatus;
// 若head节点状态为SIGNAL(-1),则自旋CAS将head节点的状态设置为0之后,才可以唤醒head结点的后继节点
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
// 执行下一次自旋
continue;
unparkSuccessor(h);
}
// 若head节点状态为0,则自旋CAS将节点状态设置为PROPAGATE(-3)
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
// 执行下一次自旋
continue;
}
// head指针在自旋期间未发生移动的话,跳出自旋
if (h == head)
break;
}
}
为什么最后需要判断(h==head)才跳出自旋?
想象2种情景:
- 第1种情景
线程thread1自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread2,当执行到(h == head)时,假如thread2唤醒后已经将head指向自己了,此时(h == head)返回false,thread1继续自旋获取到新的head节点(thread2);
thread1自旋CAS将新的head节点(thread2)的状态由SIGNAL修改为0,然后去唤醒thread2的后继线程thread3,当执行到(h == head)时,假如thread3唤醒后已经将head指向自己了,此时(h == head)返回false,thread1继续自旋获取到新的head节点(thread3),
thread1自旋CAS将新的head节点(thread3)的状态由SIGNAL修改为0,然后去唤醒thread3的后继线程thread4......
直到某个被唤醒的线程因为获取不到锁(资源被用尽)执行shouldParkAfterFailedAcquire方法被阻塞挂起,head节点才没有发生改变,此时(h == head)返回true,跳出自旋。
- 第2种情景
线程thread1自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread2,当执行到(h == head)时,假如thread2唤醒后还未来得及将head指向自己,此时(h == head)返回true,thread1停止自旋;
thread2唤醒后将执行setHeadAndPropagate方法将head指向自己,并最终进到doReleaseShared方法的自旋中;
此时,线程thread2自旋CAS将head节点的状态由SIGNAL修改为0,才唤醒后继线程thread3......
哈哈哈,是不是像thread1一样又面临了2种情景。
可以看到,整个唤醒后继节点的过程是不断嵌套,螺旋执行的,每个节点的线程都最大程度的尝试唤醒其可以唤醒的节点,而且每个线程都是唤醒的head的后继节点,head指针不断往后推进,则被唤醒尝试获取共享锁的线程越多,而新的线程一旦获取到锁,其又会执行到setHeadAndPropagate-->doReleaseShared的自旋中,加入到唤醒head后继节点的联盟大军中,直到无锁可获。
所以,整个唤醒后继节点的过程如果一场风暴一样,不得不惊叹这样的设计呀,最大程度的诠释了何为共享,就是"有肉一起吃,有酒一起喝"。
解锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
某线程执行tryReleaseShared方法成功后,会释放掉部分资源,然后执行doReleaseShared方法唤醒当前head节点的后继线程,来参与分享资源。
doReleaseShared方法前面陈述过了,这是个"唤醒风暴",它会唤醒所有可以唤醒的人来参与资源的分享。
整个获取/释放资源的过程是通过传播完成的,如最开始有10个资源,线程A、B、C分别需要5、4、3个资源。
- A线程获取到5个资源,其发现资源还剩余5个,则唤醒B线程;
- B线程获取到4个资源,其发现资源还剩余1个,唤醒C线程;
- C线程尝试取3个资源,但发现只有1个资源,继续阻塞;
- A线程释放1个资源,其发现资源还剩余2个,故唤醒C线程;
- C线程尝试取3个资源,但发现只有2个资源,继续阻塞;
- B线程释放2个资源,其发现资源还剩余4个,唤醒C线程;
- C线程获取3个资源,其发现资源还剩1个,继续唤醒后续等待的D线程;
- …
回顾整个共享锁加锁和解锁的过程,可以发现head指针至关重要,无论是加锁成功后执行setHeadAndPropagate方法进而执行doReleaseShared方法,还是线程解锁时直接执行doReleaseShared方法,其均是直接从当前队列的的head节点的后继节点开始"唤醒",而被唤醒的多个线程也是通过(h == head)判断来决定是否跳出"唤醒自旋"的。
最后,再次感叹,这个"唤醒风暴"设计得太赞了!!!