接下来看看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) // 如果最后还没有获取到,直接cancel cancelAcquire(node); } }
原来阻塞就是挂起线程呀,我们其实早就知道了,只不过验证了下。而且上面的过程和独占锁其实很类似,不过在获取到节点后,不仅将当前线程设置成了头节点,而且还唤醒了后继节点。这就是共享锁的传播性:当前节点被唤醒后,后继节点也会被唤醒。这是因为可能不止一个线程调用了await方法进行等待。
究竟是如何进行的传播与唤醒呢?走进setHeadAndPropagate来一探究竟吧
private void setHeadAndPropagate(Node node, int propagate) { Node h = head; //取出头节点 setHead(node); //将当前节点设置为头节点 //doAcquireShared中传参propagate一定大于0 //waitStatus初始为0,SIGNAL=-1; // CONDITION = -2,PROPAGATE = -3; if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) { // 取出当前节点的后继节点 Node s = node.next; //共享模式或者s为空,继续doReleaseShared if (s == null || s.isShared()) //继续唤醒下一个节点 doReleaseShared(); } }
原来就是判断是不是共享模式,是就继续调用doReleaseShared
唤醒下一个节点,doReleaseShared
后面会讲.
3 countdown源码剖析
了解完await
的底层原理,这里我们接下来看下countdown
方法的底层原理。看看它的底层调用方法
public void countDown() { sync.releaseShared(1); }
点进releaseShared
public final boolean releaseShared(int arg) { // 尝试释放锁,成功返回true // tryReleaseShared在前面讲await源码时讲过 //只有当锁数量为0时才会释放成功 if (tryReleaseShared(arg)) { // 继续唤醒后续节点 doReleaseShared(); return true; } return false; }
原来countdown
和await
最后都会调用doReleaseShared
唤醒其它节点,前文留下的悬念是时候解开了,那就看看doReleaseShared
吧
private void doReleaseShared() { for (;;) { Node h = head; // 如果头节点不为空,而且头、尾节点不相同 //说明等待队列中存在节点 if (h != null && h != tail) { // 获取头节点的等待状态 int ws = h.waitStatus; // 如果是SIGNAL(表示后继节点被挂起), //就将头节点的状态设置为初始值 if (ws == Node.SIGNAL) { if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; //失败就重来 // 当头节点被唤醒,会唤醒头节点的后继节点 unparkSuccessor(h); } // 如果头节点的等待状态是初始状态0 // 尝试将其状态设置为PROPAGATE(表示后继节点已经被唤醒) //PROPAGATE状态在setHeadAndPropagate中用到 //可以让唤醒操作向后继节点传播 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } if (h == head) // loop if head changed break; } }
可能看下来觉得还是比较凌乱,没关系,我们可以回过头对本文章的源码与本专栏的AQS源码多读几遍,这里也梳理下,方便大家理解。
- 共享锁是线程共享的,同一个时刻可能有多个线程拥有共享锁。
- 如果一个线程刚获取到了共享锁,那么在其之后等待的线程很有可能也能够获取到共享锁,因此需要传播唤醒后继节点
- 如果一个线程刚刚释放了线程锁,那么无论是共享锁还是独占锁,都需要传播唤醒后继节点。