Java并发之AQS源码分析(二)

素颜马尾好姑娘i 2022-01-16 06:35 369阅读 0赞

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

微信公众号「后端进阶」,专注后端技术分享:Java、Golang、WEB框架、分布式中间件、服务治理等等。
老司机倾囊相授,带你一路进阶,来不及解释了快上车!

我在Java并发之AQS源码分析(一)这篇文章中,从源码的角度深度剖析了 AQS 独占锁模式下的获取锁与释放锁的逻辑,如果你把这部分搞明白了,再看共享锁的实现原理,思路就会清晰很多。下面我们继续从源码中窥探共享锁的实现原理。

共享锁

获取锁

  1. public final void acquireShared(int arg) {
  2. // 尝试获取共享锁,小于0表示获取失败
  3. if (tryAcquireShared(arg) < 0)
  4. // 执行获取锁失败的逻辑
  5. doAcquireShared(arg);
  6. }

这里的 tryAcquireShared 方法是留给实现方去实现获取锁的具体逻辑的,我们主要看 doAcquireShared 方法的实现逻辑:

  1. private void doAcquireShared(int arg) {
  2. // 添加共享锁类型节点到队列中
  3. final Node node = addWaiter(Node.SHARED);
  4. boolean failed = true;
  5. try {
  6. boolean interrupted = false;
  7. for (;;) {
  8. final Node p = node.predecessor();
  9. if (p == head) {
  10. // 再次尝试获取共享锁
  11. int r = tryAcquireShared(arg);
  12. // 如果在这里成功获取共享锁,会进入共享锁唤醒逻辑
  13. if (r >= 0) {
  14. // 共享锁唤醒逻辑
  15. setHeadAndPropagate(node, r);
  16. p.next = null; // help GC
  17. if (interrupted)
  18. selfInterrupt();
  19. failed = false;
  20. return;
  21. }
  22. }
  23. // 与独占锁相同的挂起逻辑
  24. if (shouldParkAfterFailedAcquire(p, node) &&
  25. parkAndCheckInterrupt())
  26. interrupted = true;
  27. }
  28. } finally {
  29. if (failed)
  30. cancelAcquire(node);
  31. }
  32. }

看到上面的代码,是不是有一种熟悉的感觉,同样是采用了自旋机制,在线程挂起之前,不断地循环尝试获取锁,不同的是,一旦获取共享锁,会调用 setHeadAndPropagate 方法同时唤醒后继节点,实现共享模式,下面是唤醒后继节点代码逻辑:

  1. private void setHeadAndPropagate(Node node, int propagate) {
  2. // 头节点
  3. Node h = head;
  4. // 设置当前节点为新的头节点
  5. // 这里不需要加锁操作,因为获取共享锁后,会从FIFO队列中依次唤醒队列,并不会产生并发安全问题
  6. setHead(node);
  7. if (propagate > 0 || h == null || h.waitStatus < 0 ||
  8. (h = head) == null || h.waitStatus < 0) {
  9. // 后继节点
  10. Node s = node.next;
  11. // 如果后继节点为空或者后继节点为共享类型,则进行唤醒后继节点
  12. // 这里后继节点为空意思是只剩下当前头节点了
  13. if (s == null || s.isShared())
  14. doReleaseShared();
  15. }
  16. }

该方法主要做了两个重要的步骤:

  1. 将当前节点设置为新的头节点,这点很重要,这意味着当前节点的前置节点(旧头节点)已经获取共享锁了,从队列中去除;
  2. 调用 doReleaseShared 方法,它会调用 unparkSuccessor 方法唤醒后继节点。

释放锁

  1. public final boolean releaseShared(int arg) {
  2. // 由用户自行实现释放锁条件
  3. if (tryReleaseShared(arg)) {
  4. // 执行释放锁
  5. doReleaseShared();
  6. return true;
  7. }
  8. return false;
  9. }

下面是释放锁逻辑:

  1. private void doReleaseShared() {
  2. for (;;) {
  3. // 从头节点开始执行唤醒操作
  4. // 这里需要注意,如果从setHeadAndPropagate方法调用该方法,那么这里的head是新的头节点
  5. Node h = head;
  6. if (h != null && h != tail) {
  7. int ws = h.waitStatus;
  8. //表示后继节点需要被唤醒
  9. if (ws == Node.SIGNAL) {
  10. // 初始化节点状态
  11. //这里需要CAS原子操作,因为setHeadAndPropagate和releaseShared这两个方法都会顶用doReleaseShared,避免多次unpark唤醒操作
  12. if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
  13. // 如果初始化节点状态失败,继续循环执行
  14. continue; // loop to recheck cases
  15. // 执行唤醒操作
  16. unparkSuccessor(h);
  17. }
  18. //如果后继节点暂时不需要唤醒,那么当前头节点状态更新为PROPAGATE,确保后续可以传递给后继节点
  19. else if (ws == 0 &&
  20. !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
  21. continue; // loop on failed CAS
  22. }
  23. // 如果在唤醒的过程中头节点没有更改,退出循环
  24. // 这里防止其它线程又设置了头节点,说明其它线程获取了共享锁,会继续循环操作
  25. if (h == head) // loop if head changed
  26. break;
  27. }
  28. }

共享锁的释放锁逻辑比独占锁的释放锁逻辑稍微复杂,原因是共享锁需要释放队列中所有共享类型的节点,因此需要循环操作,由于释放锁过程中会涉及多个地方修改节点状态,此时需要 CAS 原子操作来并发安全。

获取共享锁流程图:

aqs_2.jpg

总结

更独占锁相比,从流程图也可看出,共享锁的主要特征是当有一个线程获取到锁之后,那么它就会依次唤醒等待队列中可以跟它共享的节点,当然这些节点也是共享锁类型。

微信公众号「Java科代表」

转载于:https://my.oschina.net/objcoding/blog/3047164

发表评论

表情:
评论列表 (有 0 条评论,369人围观)

还没有评论,来说两句吧...

相关阅读

    相关 分析:AQS

    在开始这篇源码之前,最好先看下转载整理的[这篇文章][Link 1],有很多值得学习的地方。AQS是用来构建锁或者其他同步组件的基础框架。总体来说,它使用一个 int 成员变量

    相关 并发-AQS分析

    微信搜索:“二十同学” 公众号,欢迎关注一条不一样的成长之路 一、概述   谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈