AQS深入理解 setHeadAndPropagate源码分析 JDK8

前言

Sets head of queue, and checks if successor may be waiting in shared mode, if so propagating if either propagate > 0 or PROPAGATE status was set.

此函数被共享锁操作而使用。这个函数用来将传入参数设为队列的新节点,如果传参的后继是共享模式且现在要么 共享锁有剩余(propagate > 0) 要么 PROPAGATE状态被设置,那么调用doReleaseShared。

JUC框架 系列文章目录

共享锁获取流程

比如当你调用了doAcquireShared。

    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        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;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  • 执行doAcquireShared的当前线程想要获取到共享锁。
  • addWaiter将当前线程包装成一个共享模式的node放到队尾上去。

for循环的过程分析:

  • 执行到tryAcquireShared后可能有两种情况:
    • 如果tryAcquireShared的返回值>=0,说明线程获取共享锁成功了,那么调用setHeadAndPropagate,然后函数即将返回。
    • 如果tryAcquireShared的返回值<0,说明线程获取共享锁失败了,那么调用shouldParkAfterFailedAcquire。
      • 这个shouldParkAfterFailedAcquire一般来说,得至少执行两遍才能将返回true:第一次shouldParkAfterFailedAcquirenode把前驱设置为SIGNAL状态,第二次检测到SIGNAL才返回true。
      • 既然上一条说了,shouldParkAfterFailedAcquirenode一般执行两遍,那么很有可能第二遍的时候,发现自己的前驱突然变成head了并且获取共享锁成功,又或者本来第一遍的前驱就是head但第二遍获取共享锁成功了。不用觉得第一遍的SIGNAL白设置了,因为设置前驱SIGNAL本来就是为了让前驱唤醒自己的,现在自己处于醒着的状态就获得了共享锁,那就接着执行setHeadAndPropagate就好。
      • 剩下的就是常见情况了。线程调用两次shouldParkAfterFailedAcquire,和一次parkAndCheckInterrupt后,便阻塞了。之后就只能等待别人unpark自己了,以后如果自己唤醒了,又会走以上这个流程。

总之,执行doAcquireShared的线程一定会是局部变量node所代表的那个线程(即这个node的thread成员)。

setHeadAndPropagate分析

    private void setHeadAndPropagate(Node node, long propagate) {
        Node h = head; // Record old head for check below
        setHead(node);

        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
  • 入参node所代表的线程一定是当前执行的线程,propagate则代表tryAcquireShared的返回值,由于有if (r >= 0)的保证,propagate必定为>=0,这里返回值的意思是:如果>0,说明我这次获取共享锁成功后,还有剩余共享锁可以获取;如果=0,说明我这次获取共享锁成功后,没有剩余共享锁可以获取。
  • Node h = head; setHead(node);执行完这两句,h保存了旧的head,但现在head已经变成node了。
  • h == null(h = head) == nulls == null是为了防止空指针异常发生的标准写法,但这不代表就一定会发现它们为空的情况。这里的话,h == null(h = head) == null是不可能成立,因为只要执行过addWaiter,CHL队列至少也会有一个node存在的;但s == null是可能发生的,比如node已经是队列的最后一个节点。
  • 看第一个if的判断:
    • 如果propagate > 0成立的话,说明还有剩余共享锁可以获取,那么短路后面条件。
    • 中间穿插一下doReleaseShared的介绍:它不依靠参数,直接在调用中获取head,并在一定情况unparkSuccessor这个head。但注意,unpark head的后继之后,被唤醒的线程可能因为获取不到共享锁而再次阻塞(见上一章的流程分析)。
    • 如果propagate = 0成立的话,说明没有剩余共享锁可以获取了,按理说不需要唤醒后继的。也就是说,很多情况下,调用doReleaseShared,会造成acquire thread不必要的唤醒。之所以说不必要,是因为唤醒后因为没有共享锁可以获取而再次阻塞了。
    • 继续看,如果propagate > 0不成立,而h.waitStatus < 0成立。这说明旧head的status<0。但如果你看doReleaseShared的逻辑,会发现在unparkSuccessor之前就会CAS设置head的status为0的,在unparkSuccessor也会进行一次CAS尝试,因为head的status为0代表一种中间状态(head的后继代表的线程已经唤醒,但它还没有做完工作),或者代表head是tail。而这里旧head的status<0,只能是由于doReleaseShared里的compareAndSetWaitStatus(h, 0, Node.PROPAGATE)的操作,而且由于当前执行setHeadAndPropagate的线程只会在最后一句才执行doReleaseShared,所以出现这种情况,一定是因为有另一个线程在调用doReleaseShared才能造成,而这很可能是因为在中间状态时,又有人释放了共享锁。propagate == 0只能代表当时tryAcquireShared后没有共享锁剩余,但之后的时刻很可能又有共享锁释放出来了。
      在这里插入图片描述
    • 继续看,如果propagate > 0不成立,且h.waitStatus < 0不成立,而第二个h.waitStatus < 0成立。注意,第二个h.waitStatus < 0里的h是新head(很可能就是入参node)。第一个h.waitStatus < 0不成立很正常,因为它一般为0(考虑别的线程可能不会那么碰巧读到一个中间状态)。第二个h.waitStatus < 0成立也很正常,因为只要新head不是队尾,那么新head的status肯定是SIGNAL。所以这种情况只会造成不必要的唤醒。
      在这里插入图片描述
  • 看第二个if的判断:
    • s == null完全可能成立,当node是队尾时。此时会调用doReleaseShared,但doReleaseShared里会检测队列中是否存在两个node。
    • s != nulls.isShared(),也会调用doReleaseShared。

The conservatism in both of these checks may cause unnecessary wake-ups, but only when there are multiple racing acquires/releases, so most need signals now or soon anyway.

源码注释自己也说了,if判断这么写是可能造成不必要的唤醒的。

总结

  • setHeadAndPropagate函数用来设置新head,并在一定情况下调用doReleaseShared
  • 调用doReleaseShared时,可能会造成acquire thread不必要的唤醒。个人认为,作者这么写,是为了防止一些未知的bug,毕竟当一个线程刚获得共享锁后,它的后继很可能也能获取。
  • 可以猜想,doReleaseShared的实现必须是无伤大雅的,因为有时调用它是没有必要的。
  • PROPAGATE状态存在的意义是它的符号和SIGNAL相同,都是负数,所以能用< 0检测到。因为线程刚被唤醒,但还没设置新head前,当前head的status是0,所以把0变成PROPAGATE,好让被唤醒线程可以检测到。
  • 23
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 24
    评论
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值