微信公众号「后端进阶」,专一后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。
老司机倾囊相授,带你一路进阶,来不及解释了快上车!java
我在Java并发之AQS源码分析(一)这篇文章中,从源码的角度深度剖析了 AQS 独占锁模式下的获取锁与释放锁的逻辑,若是你把这部分搞明白了,再看共享锁的实现原理,思路就会清晰不少。下面咱们继续从源码中窥探共享锁的实现原理。node
public final void acquireShared(int arg) { // 尝试获取共享锁,小于0表示获取失败 if (tryAcquireShared(arg) < 0) // 执行获取锁失败的逻辑 doAcquireShared(arg); }
这里的 tryAcquireShared 方法是留给实现方去实现获取锁的具体逻辑的,咱们主要看 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); } }
看到上面的代码,是否是有一种熟悉的感受,一样是采用了自旋机制,在线程挂起以前,不断地循环尝试获取锁,不一样的是,一旦获取共享锁,会调用 setHeadAndPropagate 方法同时唤醒后继节点,实现共享模式,下面是唤醒后继节点代码逻辑:安全
private void setHeadAndPropagate(Node node, int propagate) { // 头节点 Node h = head; // 设置当前节点为新的头节点 // 这里不须要加锁操做,由于获取共享锁后,会从FIFO队列中依次唤醒队列,并不会产生并发安全问题 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(); } }
该方法主要作了两个重要的步骤:微信
public final boolean releaseShared(int arg) { // 由用户自行实现释放锁条件 if (tryReleaseShared(arg)) { // 执行释放锁 doReleaseShared(); return true; } return false; }
下面是释放锁逻辑:并发
private void doReleaseShared() { for (;;) { // 从头节点开始执行唤醒操做 // 这里须要注意,若是从setHeadAndPropagate方法调用该方法,那么这里的head是新的头节点 Node h = head; if (h != null && h != tail) { int ws = h.waitStatus; //表示后继节点须要被唤醒 if (ws == Node.SIGNAL) { // 初始化节点状态 //这里须要CAS原子操做,由于setHeadAndPropagate和releaseShared这两个方法都会顶用doReleaseShared,避免屡次unpark唤醒操做 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) // 若是初始化节点状态失败,继续循环执行 continue; // loop to recheck cases // 执行唤醒操做 unparkSuccessor(h); } //若是后继节点暂时不须要唤醒,那么当前头节点状态更新为PROPAGATE,确保后续能够传递给后继节点 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } // 若是在唤醒的过程当中头节点没有更改,退出循环 // 这里防止其它线程又设置了头节点,说明其它线程获取了共享锁,会继续循环操做 if (h == head) // loop if head changed break; } }
共享锁的释放锁逻辑比独占锁的释放锁逻辑稍微复杂,缘由是共享锁须要释放队列中全部共享类型的节点,所以须要循环操做,因为释放锁过程当中会涉及多个地方修改节点状态,此时须要 CAS 原子操做来并发安全。框架
获取共享锁流程图:分布式
更独占锁相比,从流程图也可看出,共享锁的主要特征是当有一个线程获取到锁以后,那么它就会依次唤醒等待队列中能够跟它共享的节点,固然这些节点也是共享锁类型。oop