AQS的一个cancleAcquire方法,能看到Doug Lea大神的多少细节?

article/2025/11/6 9:05:50

目录

  • 写在前面
  • 一、调用时机
  • 二、源码分析
    • 2.1 node为tail
    • 2.2 node为中间节点
      • 2.2.1 N2的取消逻辑
      • 2.2.2 N3继N2之后取消
    • 2.3 node是头结点的后继节点
    • 2.4 并发取消的场景
  • 三、思考与总结

写在前面

第一次读AQS源码时,对cancleAcquire的理解比较肤浅(停留在方法名的字面意思上了),深入了解之后才知道有很多细节。
首先需要对AQS的同步状态管理、阻塞线程的队列管理以及线程节点的几种状态机有一定了解,这里不做赘述。
JDK版本:
在这里插入图片描述

一、调用时机

  • AQS几种获取同步状态的方式(独占式 or 共享式,超时 or 非超时,响应中断 or 不响应中断)都涉及cancleAcquire方法的调用,调用逻辑大同小异,即在线程(Node节点)阻塞的过程意外退出时,则会调用cancelAcquire()。
    在这里插入图片描述
  • 以acquireInterruptibly(独占,响应中断,非超时永久阻塞)为例:
    在这里插入图片描述
    线程阻塞的过程被中断,抛出InterruptedException,则会走到cancleAcquire的逻辑

二、源码分析

  • 完整的源码:
    private void cancelAcquire(Node node) {if (node == null)return;node.thread = null;Node pred = node.prev;while (pred.waitStatus > 0)node.prev = pred = pred.prev;Node predNext = pred.next;node.waitStatus = Node.CANCELLED;if (node == tail && compareAndSetTail(node, pred)) {compareAndSetNext(pred, predNext, null);} else {int ws;if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL ||(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&pred.thread != null) {Node next = node.next;if (next != null && next.waitStatus <= 0)compareAndSetNext(pred, predNext, next);} else {unparkSuccessor(node);}node.next = node; // help GC}}
    
  • 方法入参node即为待取消的节点, 整体看下来主要有两个作用:
    • 修改node的状态,thread置为null,ws置为Node.CANCELLED
    • node节点出队(将当前取消节点的前置非取消节点和后置非取消节点"链接"起来)
  • 具体的出队逻辑分三种情况:
    • node是尾结点
    • node是中间节点
    • node是头结点的后继结点

2.1 node为tail

  • 即图中的N2节点出队,只需修改N2节点状态,修改TAIL的指向,并断开N1到N2的next指针。
    在这里插入图片描述
    在这里插入图片描述
  • 无需断开N2到N1的prev指针,N2也会被GC回收
  • 注:ws=-1为SIGNAL,ws=1为CANCLED,ws=0为初始状态,后面不赘述

2.2 node为中间节点

2.2.1 N2的取消逻辑

这里的中间节点指node既不是tail节点,也不是head的后继节点,例如下图中的N2
在这里插入图片描述

  • N2取消的过程中,修改了N2对应Node节点的状态,并将前置非取消节点和后置非取消节点"链接"起来
    在这里插入图片描述
  • 注意这里N3对N2的prev指针并没有断开,即此时N2并没有完全出队,也不会被GC回收。这里衍生出两个问题,prev指针为什么没有断开以及是不是一直都不会断开?我们假设一个场景,N2取消之后,N3也调用了取消逻辑,借助这个场景剖析这两个问题。

2.2.2 N3继N2之后取消

N2取消之后,N3也取消的逻辑:
在这里插入图片描述

  • 看cancleAcquire源码,有这样一段逻辑:
    在这里插入图片描述
  • 正是通过上述逻辑,取消N3的过程中,断开了N3到N2的prev指针,此时N2真正出队,等着GC回收。如果N2取消时就断开了N3到N2的prev指针,那么N3取消的时候就无法通过prev遍历到N1,所以N2取消时保留了这个prev指针。
  • 后继节点N3的取消可以帮助N2完成完整的出队动作,那么如果某个节点取消之后,没有后继节点取消的场景,是如何完成出队的呢?就以N3取消的场景为例,N3的真正出队,是在N1(已经是获取了同步状态的头结点)释放同步状态唤醒其next节点(即N4)时
    在这里插入图片描述
    • 注意N4被唤醒后prev并不是head节点仍是N3,因此还会走到shouldParkAfterFailedAcquire()的逻辑中
      在这里插入图片描述
    • shouldParkAfterFailedAcquire()中,N4线程将其节点的prev指向了N1(随后尝试获取同步状态),至此N3彻底出队。
      在这里插入图片描述

总结一下:取消节点并不是调用cancleAcquire之后就彻底出队,而是保留了指向自己的prev指针,以保证后面节点一旦取消,能通过prev遍历到前面的待唤醒节点。而取消节点的真正出队有两个入口,一个是其后继节点的取消,另一个是其后继节点的唤醒。

2.3 node是头结点的后继节点

整体流程如下:
在这里插入图片描述

  • T1线程调用cancleAcquire,由于其前驱节点已是head,会直接调用unparkSuccessor,分析源码可以看出,在unparkSuccessor中会唤醒N2节点
    在这里插入图片描述
  • N2被唤醒后,参考N4前面被唤醒的情况,会断开其到N1的prev,将prev指向head,由于前驱节点已经是head,因此可以尝试获取同步状态
  • N2获取到同步状态后,更改head节点为N2,同时一行极其神奇的代码,将原head指向N1的next指针断开,至此N1才彻底出队
    在这里插入图片描述
    第一次看这个代码时不理解,因为正常情况的更改head节点,即便head的next指针不断,也不影响head本身回收,真正的关窍就是head的后继节点可能已经被取消,断掉这个next指针其实是为了后继取消节点的回收
  • 这里有个细节,N1作为head的后继结点为什么要主动调用unparkSccessor唤醒N2,而不是和中间节点取消时一样将head的next指向N2等头结点释放同步状态的时候再去唤醒N2?
    • 这里主要区别在于,中间节点取消时其前驱节点同样也在阻塞,而N1取消时其前驱节点是当前持有同步状态的线程,因此其状态有可能随时发生变化。

    • 考虑这种场景,在N1取消的过程中,当前持有同步状态的线程(head节点)正准备释放同步状态,也走到了unparkSccessor的逻辑里,并且获取到N1的waitStatus时仍为SIGNAL(N1还没执行node.waitStatus = Node.CANCELLED;)
      在这里插入图片描述
      在这里插入图片描述

      此时唤醒的节点是正在被取消的N1,此时N1如果不主动唤醒N2,后面的剧情极其狗血,N1完成了取消动作,以为什么也不需要做头结点就继续唤醒N2。。。最后的结果就是N2会一直阻塞,永远不会唤醒

    • 结合之前中间位置节点取消的case,可以看出Doug Lea大神整体的思路就是,某个节点A取消了,如果它前面有正等待唤醒的节点B,就把唤醒后面节点的任务交给B,如果自己已经是最前面的了,就要主动唤醒!

2.4 并发取消的场景

假设N2和N3同时调用cancleAcquire取消
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
综上,T2和T3结束cancleAcquire之后的状态应为:
在这里插入图片描述
参考2.2.2节中的解析,T4线程被唤醒后的会执行shouldParkAfterFailedAcquire的逻辑,将状态改为:
在这里插入图片描述
至此,N2和N3同时完成出队

三、思考与总结

可以看出,cancleAcquire和shouldParkAfterFailedAcquire两个方法的配合十分精妙,每个方法只完成自己该完成的事情。再想一下方法名表达的字面意思,一个是“取消获取同步状态”,其负责完成当前节点的取消动作,包括节点状态的改变、将前置非取消节点和后置非取消节点“链接”起来、以及某些场景下主动唤醒后继节点;而shouldParkAfterFailedAcquire的意思是,在获取同步状态失败后是不是应该挂起线程,只有当前置节点是SIGNAL时才会返回true,基于这个条件在不满足的时候会对队列的状态进行调整,清理掉那些取消的节点。

再次膜拜Doug Lea!!!


http://chatgpt.dhexx.cn/article/GDTskST0.shtml

相关文章

leaq c 汇编语言,汇编语言lea指令使用方法解析

这篇文章主要介绍了汇编语言lea指令使用方法解析,文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 lea指令变种(按大小分类): leaw #2个字节 leal #4个字节 leaq #8个字节 lea的用法: leaq a(b, c, d), %rax 首先…

微机原理——指令系统——传送类指令(MOV、LEA、LDS、LES、LAHF、SAHF、XCHG、XLAT、PUSH、POP、PUSHF、POPF)

博主联系方式&#xff1a; QQ:1540984562 QQ交流群&#xff1a;892023501 群里会有往届的smarters和电赛选手&#xff0c;群里也会不时分享一些有用的资料&#xff0c;有问题可以在群里多问问。 【没事儿可以到我主页看看】https://blog.csdn.net/qq_42604176 传送类指令 1&am…

汇编指令lea 和 mov 区别

刚开始学汇编都会接触到指令lea 和mov&#xff0c;但是这两个指令用法看起来很像&#xff0c;会经常误以为两者其实没有很大区别&#xff0c;甚至会误认为两者就是相等的&#xff0c;笔者本人也是很迷&#xff0c;而且书上对lea的讲解又十分有限&#xff0c;但是通过不懈的知识…

x86 LEA 指令

友链 参考链接&#xff1a; https://stackoverflow.com/questions/1658294/whats-the-purpose-of-the-lea-instruction 在使用OllyDBG反汇编一个exe的时候&#xff0c;看到了这样的代码 这部分对应的源代码应该是&#xff1a; return 2*xy;明明是一个乘法和加法运算&#x…

数据传送类指令(PUSH,POP,LEA)

目录 数据传送类指令 堆栈的概念&#xff1a; 进栈指令 &#xff08;PUSH&#xff09; 出栈指令&#xff08;POP&#xff09; 练习 LEA取偏移地址&#xff08;有效地址EA&#xff09;指令&#xff08;去括号&#xff09; LEA和OFFSET区别&#xff1a; 用法注意 LEA和MO…

汇编:lea指令学习

加载有效地址&#xff08;load effective address&#xff09;指令就是lea,他的指令形式就是从内存读取数据到寄存器&#xff0c;但是实际上他没有引用内存&#xff0c;而是将有效地址写入到目的的操作数&#xff0c;就像是C语言地址操作符&一样的功能&#xff0c;可以获取…

汇编中的lea指令的作用,简单清晰明了不废话!

首先看一下intel开发手册上对lea的官方解释&#xff1a; lea&#xff0c;官方解释Load Effective Address&#xff0c;即装入有效地址的意思&#xff0c;它的操作数就是地址&#xff1b; 常见的几种用法&#xff1a; 1、lea eax&#xff0c;[addr] 就是将表达式addr的值放入…

设计模式

简介 设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式可以提高代码的可复用性、可维护性、可读性、稳健性以及安全性。 项目中合理地运用设计模式可以完美地解决很多问题&#xff0c;但滥用设计模式不但不会带来任何好处&#x…

设计模式选择题复习

1.在UML提供的图中&#xff0c;&#xff08; &#xff09;用于描述系统与外部系统及用户之间的交互 A&#xff0e;用例图 B&#xff0e;类图 C&#xff0e;对象图 D&#xff0e;部署图 2.在UML提供的图中&#xff0c;&#xff08; &#xff09;用于按时间顺序描述对象之间的交…

单例设计模式

一.何为单例设计模式 如其名字&#xff0c;单例设计模式就是指的是一个类中只允许存在一个对象实例。 在java中&#xff0c;我们存在两种创建单例模式的思路&#xff1a;饿汉式和懒汉式 ①饿汉式&#xff1a;在创建类时直接创建对象实例 public class SingleHungryMan {//建立类…

设计模式在程序中的使用

OO设计的主要目的&#xff0c;在于分割责任&#xff0c;将每个模块的责任降低到合理的程度&#xff0c;并对各个模块进行封装以及降低两个模块之间的耦合度&#xff0c;达到修改一处不影响另外一处的目的。 这个原则其实并不仅仅局限于OO设计&#xff0c;只是OO设计提供了更好…

设计模式之命令模式

命令模式 命令模式的定义非命令模式实现&#xff1a;命令模式的实现&#xff1a; 命令模式的定义 说实话这个模式挺令人纠结的&#xff0c;但从这个模式的定义上来看&#xff0c;有点让人摸不到什么头脑&#xff0c;而且查看资料以后会发现还是有点稀里糊涂的&#xff0c;说懂…

《C++ 设计模式》

作者&#xff1a; 一去、二三里 个人微信号&#xff1a; iwaleon 微信公众号&#xff1a; 高效程序员 设计模式&#xff08;Design Pattern&#xff09;代表了最佳的实践&#xff0c;在面向对象的编程中被很多老鸟们反复使用。使用设计模式有很多好处&#xff1a; 可重用代码保…

设计模式课程设计

文章目录 题目要求&#xff1a; 具体作业如下&#xff1a;一、设计思路二、所用模式介绍1.简单工厂模式2.装饰模式3.观察者模式 具体实现过程程序分为三个部分: PS.代码写的比较敷衍&#xff0c;主要是应付作业用&#xff0c;请大家自行斟酌抄袭 又到了一年两度的课程设计时间&…

23种设计模式总结

一、什么是设计模式 设计模式&#xff08;Design pattern&#xff09;是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问&#xff0c;设计模式于己于他人于系统都是多赢…

设计模式之——桥接模式

设计模式&#xff1a; 前辈们对代码开发经验的总结&#xff0c;是解决特定问题的一系列套路。它不是语法规定。而是一套用来提高代码可复用性、可维护性、可读性、稳健性、以及安全性的解决方案 设计模式的本质是面向对象设计原则的实际运用&#xff0c;是对类的封装性、继承性…

面向对象程序设计

之前复习面向对象的时候整理的&#xff0c;丢出来一起分享一下。因为复习得很赶&#xff0c;只是大致的整理&#xff0c;且大部分图片来自老师的ppt&#xff0c;可能不是很准确。如果要详细了解其中的某个知识点请另外搜索。 但是老师不讲武德啊&#xff0c;明明提纲给了不按提…

MVC设计模式

MVC的全名是Model View Controller&#xff0c;是模型(Model)&#xff0d;视图(view)&#xff0d;控制器(controller)的缩写&#xff0c;是一种设计模式。它是用一种业务逻辑、数据与界面显示分离的方法来组织代码&#xff0c;将众多的业务逻辑聚集到一个部件里面&#xff0c;在…

设计模式考题复习

一.定义 设计模式六大基本原则&#xff1a; 单一职责原则&#xff1a;一个类或者一个方法只负责一项职责&#xff0c;尽量做到类的只有一个行为原因引起变化&#xff1b;里氏替换原则&#xff1a;能出现子类的地方都应该可以允许父类出现&#xff0c;也就是子类可以扩展父类的…

一文带你通俗理解23种软件设计模式(推荐收藏,适合小白学习,附带C++例程完整源码)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 一、设计模式是什么&#xff1f; 设计模式是为了解决在软件开发过程中遇到的某些问题而形成的思想。同一场景有多种设计模式可以应…