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

2020-05-21 00:06:50 +08:00
 amiwrong123
    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. 感觉好多疑问,都不知如何说起...
2455 次点击
所在节点    Java
4 条回复
amiwrong123
2020-05-21 18:36:06 +08:00
我好后悔,我是不是应该弄个吸引人的标题的…
guyeu
2020-05-22 15:36:18 +08:00
1. 输入不确定的情况下,就是得检查;
2. 这纯粹是语言基础问题。。检查两遍是因为检查的是俩不同的对象;

1. 运行期间头结点是有可能变的;
2. 啥叫从 SIGNAL 到 0 再到 PROPAGATE,那特么是俩逻辑分支;
3. 别说了;
luckyrayyy
2020-05-22 15:44:11 +08:00
https://mingshan.fun/2019/02/02/aqs-shared/
你看这篇文章,检查两边是因为这是两个对象,一个是之前的头结点,一个是新的头结点。
amiwrong123
2020-05-23 10:45:41 +08:00
@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`就有点想不通,感觉他只会造成不必要的唤醒?好奇怪

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/673816

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX