首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
amiwrong123
V2EX  ›  Java

AQS 里的 setHeadAndPropagate 以及关于 PROPAGATE 信号的疑问?

  •  
  •   amiwrong123 · 51 天前 · 906 次点击
    这是一个创建于 51 天前的主题,其中的信息可能已经有所发展或是发生改变。
        private void setHeadAndPropagate(Node node, int propagate) {
            Node h = head; // Record old head for check below
            setHead(node);
            /*
             * Try to signal next queued node if:
             *   Propagation was indicated by caller,
             *     or was recorded (as h.waitStatus either before
             *     or after setHead) by a previous operation
             *     (note: this uses sign-check of waitStatus because
             *      PROPAGATE status may transition to SIGNAL.)
             * and
             *   The next node is waiting in shared mode,
             *     or we don't know, because it appears null
             *
             * 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 (propagate > 0 || h == null || h.waitStatus < 0 ||
                (h = head) == null || h.waitStatus < 0) {
                Node s = node.next;
                if (s == null || s.isShared())
                    doReleaseShared();
            }
        }
    
    1. 上面的 h == null (h = head) == null s == null 看起来好像只是为了防止空指针异常,还是真的会出现这些情况,这些情况都是啥场景啊?
    2. h == null 和(h = head) == null 为啥要检查两遍,看起来是 下一个时间节点,head 就可能变了,但如果考虑这种情况,检查两遍 难道就够吗?
        private void doReleaseShared() {
            /*
             * Ensure that a release propagates, even if there are other
             * in-progress acquires/releases.  This proceeds in the usual
             * way of trying to unparkSuccessor of head if it needs
             * signal. But if it does not, status is set to PROPAGATE to
             * ensure that upon release, propagation continues.
             * Additionally, we must loop in case a new node is added
             * while we are doing this. Also, unlike other uses of
             * unparkSuccessor, we need to know if CAS to reset status
             * fails, if so rechecking.
             */
            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;
            }
        }
    

    上面引入的 PROPAGATE 状态,就是为了 setHeadAndPropagate 能够检测到而存在的吧。

    1. 上面这个函数看起来 一般情况下,只会执行一次循环(头节点没有变)
    2. 为啥要从 SIGNAL 到 0 再到 PROPAGATE 呢,是为了防 unparkSuccessor 一手吗?
    3. 感觉好多疑问,都不知如何说起...
    4 条回复    2020-05-23 10:45:41 +08:00
    amiwrong123
        1
    amiwrong123   51 天前 via Android
    我好后悔,我是不是应该弄个吸引人的标题的…
    guyeu
        2
    guyeu   50 天前
    1. 输入不确定的情况下,就是得检查;
    2. 这纯粹是语言基础问题。。检查两遍是因为检查的是俩不同的对象;

    1. 运行期间头结点是有可能变的;
    2. 啥叫从 SIGNAL 到 0 再到 PROPAGATE,那特么是俩逻辑分支;
    3. 别说了;
    luckyrayyy
        3
    luckyrayyy   50 天前
    https://mingshan.fun/2019/02/02/aqs-shared/
    你看这篇文章,检查两边是因为这是两个对象,一个是之前的头结点,一个是新的头结点。
    amiwrong123
        4
    amiwrong123   49 天前 via Android
    @guyeu
    @luckyrayyy
    谢谢回复,之前这块确实想错了,看来以后发帖前还是得看仔细,我承认错误。

    不过看完还是有疑问。分析如下


    - 入参`node`所代表的线程(这个 node 的 Thread 成员)一定是当前执行的线程,
    - 看第一个 if 的判断:
    - 如果`propagate > 0`成立的话,说明还有剩余共享锁可以获取,那么短路后面条件。
    - 如果`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`不成立,且`h.waitStatus < 0`不成立,而第二个`h.waitStatus < 0`成立。注意,第二个`h.waitStatus < 0`里的 h 是新 head (很可能就是入参 node )。第一个`h.waitStatus < 0`不成立很正常,因为它一般为 0 。第二个`h.waitStatus < 0`成立也很正常,因为只要新 head 不是队尾,那么新 head 的 status 肯定是 SIGNAL 。所以这种情况只会造成不必要的唤醒。


    简单的说,我认为,检查第一个`h.waitStatus < 0`,是因为被唤醒的线程处于中间状态,而 doReleaseShared 在这个中间状态,只会设置`compareAndSetWaitStatus(h, 0, Node.PROPAGATE)`,不会调用 unparkSuccessor(h);,因为线程已经被唤醒了。

    但检查第二个`h.waitStatus < 0`就有点想不通,感觉他只会造成不必要的唤醒?好奇怪
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2661 人在线   最高记录 5168   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 17ms · UTC 14:14 · PVG 22:14 · LAX 07:14 · JFK 10:14
    ♥ Do have faith in what you're doing.