AQS核心流程解析-cancelAcquire方法

cancelAcquire方法的重要性体现在,在获取锁的方法中,异常处理都需要用到他:
cancleAquire方法的调用情况
那么我们来分析一下这个方法的作用,先对他的作用做个总结:

  1. 处理当前取消节点的状态;
  2. 将当前取消节点的前置非取消节点和后置非取消节点"链接"起来;
  3. 如果前置节点释放了锁,那么当前取消节点承担起后续节点的唤醒职责。

先贴下源码:

    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;
        }
    }
  • 作用1.处理当前取消节点的状态
node.thread = null;
node.waitStatus = Node.CANCELLED;

当前节点需要取消,那么就需要对节点的状态进行修改,这两行代码就是起到这个作用的;

  • 作用2.将当前取消节点的前置非取消节点和后置非取消节点"链接"起来;
    由于AQS的线程队列是一个双向链表,所以想要删除一个节点,其实就是修改这个节点的前后指针,我们用下图说明:
    队列初始状态
    假设现在队列的状态如上图所示,取消的节点有可能n1~n6的任何一个。
    首先我们需要确认的是"有效的"前置节点:
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

这段代码用来找到前置的非取消节点,那疑问就来了,为什么不需要向后找到第一个非取消节点呢?
而是只往前找,不往后找呢?这个问题先放一放,先往下看。

如果说当前待取消的节点是n6节点(if (node == tail),说明什么?说明他没有后续节点了,做了下面两步:

  1. 修改尾指针:compareAndSetTail(node, pred),指向前置的第一个非取消节点;
  2. 将新的尾节点的next指针置空:compareAndSetNext(pred, predNext, null);

结合上图的结果就是(假设n4、n5都是取消节点):
如果取消的是尾节点效果图

n4、n5节点没有被引用了,那么下次gc会被回收。

如果取消的是n4节点,并且是第一个出现取消的,那么我们认为其他节点都不是取消的,先看看会发生什么
这种情况会执行这个分支:

if (pred != head &&((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) {

做完之后的结果是这样:
如果取消的是n4节点
这个时候如果再取消n5节点会是这样:
如果再取消n5节点
这个时候n4就会被gc回收了。
而且我们从上面两个图可以看出来,节点取消会保留一个从后往前的单向链接,这也解释了为什么搜索前置有效节点都是从后往前的。

如果待取消的是n1节点,且n2节点是取消节点,则会直接执行unparkSuccessor(node);,这个过程会从tail指针开始从后往前,找到最靠近头的有效(非取消)节点,唤醒这个线程。
如果待取消的是n1节点示意图
补充一句,执行cancleAcquire一定是没有获取到锁的,所以这个方法里面并没有解锁的步骤。
取消n1还会引发对n3的唤醒。
这点我们需要单独分析下:

唤醒后继节点的条件是:

  1. pred == head或者pred.thread == null,这两个其实含义差不多,都暗示了,自己是第一个节点,也就是取消了n1节点;
  2. 或者((ws = pred.waitStatus) != Node.SIGNAL 并且 (ws >0 || compareAndSetWaitStatus(pred, ws, Node.SIGNAL) == false)):ws这里取前置节点的状态,翻译过来就是前置节点是取消节点或者compareAndSetWaitStatus失败,而compareAndSetWaitStatus失败则发生在高并发时,前置线程突然释放锁;

那你想想这两种情况,是不是都代表了,前置节点已经释放锁,不再参与锁竞争,正常情况下,前置节点释放锁,需要唤醒后续SIGNAL节点,但是"瞬息万变"的环境下,如果他们释放锁的时候,当前取消节点还没有将状态改成CANCLE(node.waitStatus = Node.CANCELLED还没执行到),还是SIGNAL,那么前置节点以为自己唤醒工作已经做过了,但是他是对我这个"躺平"的节点唤醒,不起效果,这个时候后面的节点怎么办,我这个待取消节点如果不唤醒他们,那么后续节点将永远不会被唤醒,则整个队列都将处于阻塞状态。

还有一个问题没有解释,就是取消节点保持了一个向前的引用,可以看看之前的几个图,那么这些节点不会立刻被垃圾回收掉,怎么办,就让这些无效节点一直存在队列中么?

这个时候又得回忆回忆之前总结的知识了,答案在shouldParkAfterFailedAcquire中,这个方法中有一段逻辑:

int ws = pred.waitStatus;
if (ws > 0) {
	do {
		node.prev = pred = pred.prev;
	} while (pred.waitStatus > 0);
	pred.next = node;

用语言描述,就是会将当前节点的prev链到前面最近的非取消节点上。如下图所示,取消n4、n5节点后,如果n6被唤醒或者执行自旋,那么都会再次执行shouldParkAfterFailedAcquire方法,进而n6.prev会被更新,这个时候n4、n5彻底从链表上断开了,会被垃圾回收掉,所以"不是不报,时候未到"而已。
在这里插入图片描述

  • 8
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
AQS的acquireQueued方法是在tryAcquire执行失败后,用于将当前线程加入到等待队列,并在适当的时候阻塞线程。具体流程如下: 1. 首先,使用addWaiter方法将当前线程添加到等待队列中,该方法会创建一个节点并将其插入到等待队列的尾部。 2. 然后,调用shouldParkAfterFailedAcquire方法来判断是否应该阻塞当前线程。该方法会根据前驱节点的等待状态来决定是否需要阻塞当前线程。 - 如果前驱节点的等待状态为Node.SIGNAL,表示前驱节点释放锁之后会唤醒当前节点,此时应该阻塞当前线程。 - 如果前驱节点的等待状态大于0,表示前驱节点取消了等待,需要将当前节点的前驱节点设置为前驱节点的前驱节点,直到找到一个等待状态不大于0的节点为止。然后将当前节点插入到该节点之后。 - 如果前驱节点的等待状态既不是Node.SIGNAL,也不大于0,则使用compareAndSetWaitStatus方法将前驱节点的等待状态设置为Node.SIGNAL,表示当前节点需要被唤醒。 3. 最后,根据shouldParkAfterFailedAcquire方法的返回值来判断是否需要阻塞当前线程。如果shouldParkAfterFailedAcquire方法返回false,表示不需要阻塞当前线程,则acquireQueued方法会一直自旋直到成功获取锁为止。如果shouldParkAfterFailedAcquire方法返回true,表示需要阻塞当前线程,则调用LockSupport.park方法阻塞当前线程,直到被唤醒为止。 <span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [AQS核心流程解析-acquire方法](https://blog.csdn.net/IToBeNo_1/article/details/123404852)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值