AQS深入理解 shouldParkAfterFailedAcquire源码分析 状态为0或PROPAGATE的情况分析

流程分析

JUC框架 系列文章目录

谁是调用shouldParkAfterFailedAcquire的线程

以获取独占锁的方法acquireQueued为例

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 执行到这里,说明
                // 已经尝试过获取锁了,但还是失败了(当然有可能是因为p != head)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

首先要知道调用acquireQueued的线程一定是node的代表线程,也就是最开始调用lock()方法的那个线程,在acquireQueued的死循环中,尽管可能 重复着 阻塞和被唤醒 的过程,但不管怎么说,执行acquireQueued的任何代码的线程,一定是参数node的代表线程。

acquireQueued相对,再以获取共享锁的doAcquireShared方法为例:

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);//注意,这个node在这里
        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;
                    }
                }
                // 执行到这里,说明
                // 已经尝试过获取锁了,但还是失败了(当然有可能是因为p != head)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

同样的,执行doAcquireShared的任何代码的线程,一定是参数node的代表线程。其次你也会发现,doAcquireShared的方法逻辑,和acquireQueued方法的逻辑大体一致,只是在获取锁成功后干的事情略有不同:

  • 获取独占锁成功是,tryAcquire返回了true,但调用的是setHead
  • 获取共享锁成功是,tryAcquireShared返回了true,但调用的是setHeadAndPropagate
  • 另外,补上中断状态的时机不同。共享锁补上的时机是直接在doAcquireShared里,而独占锁补上中断状态的时机是,acquireQueued函数返回后,在acquire里做的。

总结:

  • 执行shouldParkAfterFailedAcquire的线程,一定是参数node的代表线程,不管是独占锁还是共享锁。
  • 执行shouldParkAfterFailedAcquire前,线程在此次循环中,已经尝试过获取锁了,但还是失败了。

什么时候会从parkAndCheckInterrupt中唤醒

还是以独占锁和共享锁二者一起考虑。

  • 最通常的情况,head节点的代表线程即exclusiveOwnerThread这个线程,唤醒了阻塞在parkAndCheckInterrupt的线程,此时这个阻塞线程一定是head的后继。
  • 其他情况,阻塞在parkAndCheckInterrupt的线程被中断了,此时线程马上被唤醒,继续for循环。任何时候别的线程都可以来中断阻塞线程。
  • 阻塞在parkAndCheckInterrupt的线程超时了,此时线程马上被唤醒,继续for循环。

总结:

  • 考虑所有情况的话,阻塞在parkAndCheckInterrupt的线程可以在任何时候被唤醒。

释放锁后,唤醒head后继的条件

首先分析独占锁:

public final boolean release(int arg) {
    if (tryRelease(arg)) {//这里已经释放了独占锁
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

可见唤醒head后继的条件是,head的状态不为0就可以了。

    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

根据unparkSuccessor的逻辑可知,在独占锁中,head的状态为0(因为if (ws < 0) compareAndSetWaitStatus(node, ws, 0);),代表head的后继节点即将被唤醒,或者已经被唤醒。head的状态为0属于一种中间状态,因为:

  • 如果head的后继唤醒后能获得锁,那么head的后继就会成为新head,只要新head不为tail,那么新head的状态一般都为SIGNAL
  • 如果head的后继唤醒后不能获得锁,那么这个线程又会执行shouldParkAfterFailedAcquire,再把head的状态置为SIGNAL

再看共享锁:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {//这里已经释放了共享锁
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

可见唤醒head后继的条件稍微有点苛刻,必须head的状态为SIGNAL且设置状态从SIGNAL变成0成功,才可以去唤醒head后继。

总之:

  • head的状态为0,代表head的后继节点即将被唤醒,或者已经被唤醒。
  • head的状态为0,属于一种中间状态。
  • 最重要的,head的状态为0,代表有人刚释放了锁

shouldParkAfterFailedAcquire源码分析

本函数,简单的讲就是把node的有效前驱(有效是指node不是CANCELLED的)找到,并且将有效前驱的状态设置为SIGNAL,之后便返回true代表马上可以阻塞了。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点已经设置了SIGNAL,闹钟已经设好,现在我可以安心睡觉(阻塞)了。
             * 如果前驱变成了head,并且head的代表线程exclusiveOwnerThread释放了锁,
             * 就会来根据这个SIGNAL来唤醒自己
             */
            return true;
        if (ws > 0) {
            /*
             * 发现传入的前驱的状态大于0,即CANCELLED。说明前驱节点已经因为超时或响应了中断,
             * 而取消了自己。所以需要跨越掉这些CANCELLED节点,直到找到一个<=0的节点
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 进入这个分支,ws只能是0或PROPAGATE。
             * CAS设置ws为SIGNAL
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
  • 如果前驱节点的状态为SIGNAL,说明闹钟标志已设好,返回true表示设置完毕。
  • 如果前驱节点的状态为CANCELLED,说明前驱节点本身不再等待了,需要跨越这些节点,然后找到一个有效节点,再把node和这个有效节点的前驱后继连接好。
  • 如果是其他情况,那么CAS尝试设置前驱节点为SIGNAL

由于shouldParkAfterFailedAcquire函数在acquireQueued的调用中处于一个死循环中(独占锁对应的是acquireQueued里的死循环,共享锁对应的是doAcquireShared的死循环),且因为shouldParkAfterFailedAcquire函数若返回false,那么此函数必将至少执行两次才能阻塞自己。

  • shouldParkAfterFailedAcquire只有在检测到前驱的状态为SIGNAL才能返回true,只有true才会执行到parkAndCheckInterrupt
  • shouldParkAfterFailedAcquire返回false后,进入下一次循环,当前线程又会再次尝试获取锁(p == head && tryAcquire(arg))。或者说,每次执行shouldParkAfterFailedAcquire,都说明当前循环 尝试过获取锁了,但失败了。
  • 如果刚开始前驱的状态为0,那么需要第一次执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL)返回false并进入下一次循环,第二次才能进入if (ws == Node.SIGNAL)分支,所以说 至少执行两次。
  • 死循环保证了最终一定能设置前驱为SIGNAL成功的。(考虑当前线程一直不能获取到锁)

关于shouldParkAfterFailedAcquire的疑问

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	// 为什么检测到0或PROPAGATE后,一定要设置成SIGNAL,
        	// 然后继续下一次循环(因为返回的false)。直接返回true不行吗
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

为什么检测到0后,一定要设置成SIGNAL,然后继续下一次循环。直接返回true不行吗

首先得思考,什么时候node的前驱pred的状态为0。

node刚成为新队尾,但还没有将旧队尾的状态设置为SIGNAL

这种情况是最常见的,比如现在AQS的等待队列中有很多node正在等待,当前线程现在刚执行完毕addWaiter(node刚成为新队尾),然后现在开始执行获取锁的死循环(独占锁对应的是acquireQueued里的死循环,共享锁对应的是doAcquireShared的死循环),此时node的前驱,也就是旧队尾的状态肯定还是0(也就是默认初始化的值),然后死循环执行两次,第一次执行shouldParkAfterFailedAcquire自然会检测到前驱状态为0,然后将0设置为SIGNAL;第二次执行shouldParkAfterFailedAcquire,直接返回true。
在这里插入图片描述
很明显,这种情况线程会进行if (p == head && tryAcquire(arg))这样的判断两次(共享锁也是这样,只不过是分开写的)。

  • if (p == head && tryAcquire(arg))判断两次,是有好处的。
  • 在获取共享锁的死循环,每次都会重新获取node的前驱(node.predecessor()
  • 上图中,也有可能在第三步和第四步之间,从head到新tail之间的node都取消掉了,使得新tail的前驱变成了head,说不定此时获取锁也能成功呢,所以这第二次if (p == head && tryAcquire(arg))判断是很有必要的。

node的前驱为head

  • 除了上一章的情况外,pred作为一个肯定不为队尾的节点,它的状态为0的情况,只能是本文章节释放锁后,唤醒head后继的条件中,所说的中间状态。这种中间状态只可能在head上出现,所以pred肯定是head。所以node现在是head后继。

再结合前面总结出的两个结论:

  • 考虑所有情况的话,阻塞在parkAndCheckInterrupt的线程可以在任何时候被唤醒。
  • 执行shouldParkAfterFailedAcquire前,线程在此次循环中,已经尝试过获取锁了,但还是失败了。

那么shouldParkAfterFailedAcquire中检测到0的情况可以是下面:
在这里插入图片描述
考虑本章场景,在shouldParkAfterFailedAcquire中,如果检测到node的前驱的状态为0,那么设置状态为SIGNAL,但设置状态其实是无所谓的。重点在于,这样shouldParkAfterFailedAcquire会返回false,继续下一次循环,也就能再一次尝试获取锁。
按照本章场景,再一次尝试获取锁,肯定能成功。

总结

检测到0后,一定要设置成SIGNAL的原因:

  • 设置前驱状态为SIGNAL,以便当前线程阻塞后,前驱能根据SIGNAL状态来唤醒自己。(具体看章节释放锁后,唤醒head后继的条件

设置成SIGNAL后会返回false的原因:

  • 返回false后,下一次循环开始,会重新获取node的前驱,前驱如果就是head,那么还会重新尝试获取锁。这次重新尝试是有必要的,某些场景下,重新尝试获取锁会成功。

为什么检测到PROPAGATE后,一定要设置成SIGNAL,然后继续下一次循环。直接返回true不行吗

PROPAGATE只能在使用共享锁的时候出现,并且只可能设置在head上。

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))//设置在head上
                continue;                
        }
        if (h == head)                   
            break;
    }
}

在这里插入图片描述
其实本章场景和上一章类似,当获取锁的下一次循环开始后,会发现再一次获取锁,就能成功了。
总结原因和上一章一样,这里就不写了。

总结

  • 对于非队尾节点,如果它的状态为0或PROPAGATE,那么它肯定是head。
  • 等待队列中有多个节点时,如果head的状态为0或PROPAGATE,说明head处于一种中间状态,且此时有线程刚才释放锁了。而对于acquire thread来说,如果检测到这种状态,说明再次acquire是极有可能获得到锁的。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值