• Java并发编程第12讲——cancelAcquire()流程详解及acquire方法总结


    上篇文章介绍了AQS的设计思想以及独占式获取和释放同步状态的源码分析,但是还不够,一是感觉有点零零散散,二是里面还有很多细节没介绍到——比如cancelAcquire()方法(重点),迫于篇幅原因,今天就把它放到这篇文章里,继续深入AQS!

    一、acquire方法

    源码的分析在上一篇文章,感兴趣的同学可以去看一下,我的建议是两篇文章一起看。

    1.1 几个状态(重点)

    ps:waitStatus>0说明等待状态时CANCELLED,waitStatus<0为其它状态。

    1. //表示线程已取消:由于在同步队列中等待的线程等待超时或中断
    2. //需要从同步队列中取消等待,节点进入该状态将不会变化(即要移除/跳过的节点)
    3. static final int CANCELLED =  1;
    4. //表示后继节点处于park,需要唤醒:后继节点的线程处于park,而当前节点
    5. //的线程如果进行释放或者被取消,将会通知(signal)后继节点。
    6. static final int SIGNAL = -1;
    7. //表示线程正在等待状态:即节点在等待队列中,节点线程在Condition上,
    8. //当其他线程对Condition调用signal方法后,该节点会从条件队列中转移到同步队列中
    9. static final int CONDITION = -2;
    10. //表示下一次共享模式同步状态会无条件地传播下去
    11. static final int PROPAGATE = -3;
    12. //节点的等待状态,即上面的CANCELLED/SIGNAL/CONDITION/PROPAGATE,初始值为0
    13. volatile int waitStatus;

    1.2 acquire()流程图及分析

    基本流程描述:

    • 调用子类重写的tryAcquire方法尝试获取同步状态,若成功则返回,反之进入addWaiter方法
    • 基于当前线程新建一个Node节点,若队列不为空则将Node节点CAS操作挂在队列尾部,队列为空或CAS失败进入enq方法
    • 查看队列是否已经初始化,若没有则优先初始化队列(自旋),随后将Node节点以CAS的方式插入队列,CAS失败则继续自旋,反之进入acquireQueued方法
    • 若Node的前驱节点为头节点,且再次tryAcquire()成功,则将Node设置为头节点,并结束自旋。若两个条件任意一个失败则进入shuoldParkAfterFailedAcquire方法
    • 若Node的前驱节点等待状态为SIGNAL,则调用parkAndCheckInterrupt方法将当前线程阻塞,若当前线程的中断状态为ture则将acquireQueued的返回值置为true,并继续自旋;反之则判断Node的前驱节点的等待状态是否为CANCELLED,若不是则CAS尝试将Node的前驱节点等待状态改为SIGNAL,并继续自旋;若是则说明这个前驱节点无效,直接跳过该节点并找一个非CANCELLED节点作为Node的前驱节点,并结束自旋(acquireQueued方法结束)。
    • acquireQueued方法返回ture则说明当前线程需要被中断(也就是Node节点的前面还有节点在排队,还没轮到Node节点)。
    • 若在acquireQueued方法中出现异常,则会调用cancelAcquire方法进行该节点的取消逻辑,这也是我们今天的重点,下面会具体分析。

    二、cancelAcquire方法

    上篇文章在分析它的源码时就感觉有点懵懵的,很多地方都不太理解(那面试的时候怎么跟面试官battle啊😁),那么今天就深入的分析一下。

    2.1 源码

    再次附上源码,方便观看。

    在acquireQueued方法中出现异常会走cancelAcquire方法取消正在进行acquire的尝试,以防止死锁或长时间的等待。这里我把它分为两个红框,下面图解流程时会用到。

    2.2 流程图 

    解释一下颜色代表的意思:

    • 紫色——方法开始和结束。
    • 橙色——断开与取消结点联系的执行逻辑。
    • 黄色——node结点为tail结点执行的逻辑。
    • 蓝色——node结点不为tail结点执行的逻辑(为head结点的后继结点或中间结点)。
    • 其它——一般逻辑。

    2.3 Node为尾节点

    初始状态:即N1为Node节点

    执行第一个红框:  

    • 将Node结点Thread置空。
    • Node节点的前驱结点N2的等待状态为CANCELLED,所以断开N1到N2的联系,并与N3建立联系。
    • 将pred指向N3结点,predNext指向N2(这里不是node节点)并把Node结点的等待状态置为CANCELLED。

     执行第二个红框:

    • 将pred设置为tail节点。
    • 断开N3到N2结点的联系。
    • 最后N1和N2节点会被GC回收。

    2.4 node为中间节点

    2.4.1 N3节点取消流程

    初始状态:Node节点为中间节点,既不是tail节点,也不是head节点的后继节点。

    执行第一个红框:

    • 将node的Thread置为null。
    • pred指向N4,preNext指向N3,也就是node节点。
    • 将node的等待状态置为CANCELLED。

     执行第二个红框:

    • next指向node节点的后继节点N2。
    • CAS将N4的后继节点置为N2。
    • Node的后继节点指向自己。

    注意:此时N2对N3的指针还没有断开,这就意味着N2并不会被GC回收,那么N2对N3的引用为什么不断开?当时作者也有点不理解,直到...假设N2也调用了cancelAcquire方法,下面一起来看一下。

    2.4.2 继N3取消后N2取消逻辑

    初始状态:N2为取消节点,这里就不作解释了。

     

    执行第一个红框:

     执行第二个红框:

    • N2执行完取消逻辑后,N3就会被GC回收。这里我们思考一下如果N3取消逻辑执行完之后就断开N2到N3的prev指针会发生什么?很简单,N2就遍历不到它前面的结点了,所以N3在取消时保留了N2到N3的指针。
    • 再思考一个问题,N3被GC回收了,要是N2执行取消逻辑后,没有后继结点取消了,那N2如何被GC回收回收呢?

    2.4.3 继N2取消后N2被GC回收逻辑

    N2被GC回收其实是在N4结点成功获取同步状态且释放同步状态,并唤醒其后继节点N1时完成的,我们来看一下。

    此时的N4为head结点,N1为node结点。

    我们再来回顾下acquireQueued方法。  

    注意此时N1的prev还是N2,所以会执行shouldParkAfterFailedAcquire方法。

    至此N2也会被GC回收,T4继续自旋,直到成功获取同步状态或出现异常。

    • 所以取消节点被GC回收有两种情形:一是后继结点取消,二是后继结点被唤醒。

    2.5 node为头节点的后继结点

    2.5.1 N3的取消逻辑

    初始状态:

    执行第一个红框:  

    执行第二个红框:会调用unparkSuccessor方法  

     

    2.5.2 继N3取消逻辑N2被唤醒

    N2被唤醒尝试获取同步状态,也就是执行acquireQueued方法。

    初始状态:

    执行acquireQueued方法:

    • 参考2.4.3 继N2取消后N2被GC回收逻辑,会调用shuldParkAfterFailedAcqquire断开N2到N3的指针。
    • 然后回将pred的后继指针指向N2。

    • 返回false,T2接着自旋,假设tryAcquire成功,执行setHead方法。

    • 将head指针执行N2。
    • 将node结点thread置为null。
    • 将node的prev指针置为null。

    接着有一段神奇的代码,将原head指向N2的next指针断开。  

    至此,原head结点完全脱离队列,等待GC回收。但不执行p.next=null似乎也符合GC的条件,那为什么要执行呢?

    如果不执行p.next=null,垃圾回收器也能自动检测并回收,但这个过程相比较而言会更耗时。也就是如果p.next仍然引用N2,那么可能会遍历整个链表来标记垃圾,这就会花费更多的时间和资源才能发现并回收p结点。执行p.next=null可以明确地告诉垃圾回收器,与p关联的结点均为垃圾,并加速回收过程。

    三、总结

    • cancelAcquire方法就负责取消结点的逻辑,即将前置结点等待状态、线程置空、非取消和后置非取消结点联系起来、或在特定场景下唤醒后继结点。
    • shouldParkFailedAcquire方法的作用就是挂起线程和队列调整进而GC回收取消节点,即当前结点前驱节点的等待状态为SIGNAL时,返回true,将当前线程挂起。反之会调整队列将取消结点进行GC回收。

    还有setHead方法添加头节点(初始化队列)和删除头节点,p.next=null加速GC回收等等,每个方法甚至每段代码都配合的十分精妙,我只能说一句🐂🍺。

    End:希望对大家有所帮助,如果有纰漏或者更好的想法,请您一定不要吝啬你的赐教🙋。 

  • 相关阅读:
    4. Python 函数进阶(多返回值、多种传参方式、匿名函数)
    四、一起学习Java 对象和类
    Leetcode 【373. 查找和最小的 K 对数字】
    查找子字符串s1在字符串s中最后出现的位置rindex()方法
    Web开发:ASP.NET CORE的前端demo(纯前端)
    android studio avd加载自己编译的镜像
    11.13 训练周记
    【无标题】
    航拍倾斜摄影 Web 3D GIS 数字孪生智慧火电厂
    MySQL MHA高可用配置及故障切换
  • 原文地址:https://blog.csdn.net/weixin_45433817/article/details/134538055