AQS(二)共享锁(基于JDK 8)

23 篇文章 1 订阅

1 介绍

上一篇文章,讲了独占锁,AQS(一)独占锁(基于JDK 8),本篇只讲共享锁。

某些共享锁使用的方法在独占锁中已经出现了,不再介绍,请读者自行去上面查看。

在 Semaphore 中,acquire 调用的是 acquireSharedInterruptibly,release 调用的是 releaseShared;在 CountDownLatch 中,await 有两个,无参数的是acquireSharedInterruptibly,有参数的是 tryAcquireSharedNanos,countDown 调用 releaseShared。

这些各种获取和释放的区别只在于对于异常的处理和是否有超时处理,为了简单,下面讲解的两个方法仍然是记录异常但不抛出和没有超时的最普通的方法。

2 锁的获取 acquireShared

在 AQS 的分析中,需要先把流程理顺,然后再具体分析。不用关心其中的的模板方法是怎么实现的,假定已经实现了功能即可。

流程如下:首先尝试获取(tryAcquireShared),失败后将当前线程包装到一个 Node 中,插入同步队列尾部,并在队列中不断自旋尝试()doAcquireShared,满足堵塞条件则会堵塞以减轻自旋的 CPU 消耗,如果堵塞后被唤醒会继续自旋尝试,直到成功后设置为头部然后继续传播(setHeadAndPropagate),并根据中断情况设置中断值(selfInterrupt);或者因为异常抛出没有成功,则取消该线程所在的节点(cancelAcquire)。

    /**
     * Acquires in shared mode, ignoring interrupts.  Implemented by
     * first invoking at least once {@link #tryAcquireShared},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquireShared} until success.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquireShared} but is otherwise uninterpreted
     *        and can represent anything you like.
     */
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

2.1 doAcquireShared

先使用 addWaiter 封装当前线程,然后循环尝试。如果 node 前一个节点是 head,尝试 tryAcquireShared,返回值>=0,表示已经成功,将当前 node 设置为头部的同时,唤醒后续节点,并根据中断情况设置中断状态;失败的话,检查 shouldParkAfterFailedAcquire,如果返回 true,表示可以堵塞,执行 parkAndCheckInterrupt,堵塞线程,直到其他线程唤醒该 node,唤醒后继续执行 for 循环。

如果存在抛出异常的情况,finally 中的 if 为 true,则取消 node。

    /**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
      	// 将节点包装一下,类型为 SHARED
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
              	// 如果前一个节点是 head,会尝试一下
                if (p == head) {
                    int r = tryAcquireShared(arg);
                  	// r>=0 成功,r表示剩余资源
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                      	// 在这里会根据中断情况决定是否自中断
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
              	// should... 返回能否堵塞,如果能,执行park...
              	// park... 的返回堵塞期间是否被中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

2.2 setHeadAndPropagate

个人对 if 的理解如下:除去判断 null,则可以简化为 if(propagate > 0 || h.waitStatus < 0 ||(h = head) h.waitStatus < 0)if(s.isShared()),前一个 if 是说如果传入的资源 propagate>0 或者 head 设置前或设置后状态小于 0(小于0是因为 PROPAGATE 可能会变成 SIGNAL),则可以检查下一个节点 s 是否是共享的,是的话则执行唤醒 doReleaseShared。

    /**
     * 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.
     *
     * @param node the node
     * @param propagate the return value from a tryAcquireShared
     */
		// 将 node设置为头部,如果剩余资源 propagate>0 或者节点状态小于0
		// 且后续节点是 shared,则 doReleaseShared
    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();
        }
    }

2.3 和独占锁的比较

在独占锁中使用的 acquire,先尝试一次 tryAcquire,如果失败,才会 addWaiter,以及 acquireQueued;如果中断的话,执行 selfInterrupt

在共享锁中使用 acquireShared,同样先尝试一次 tryAcquireShared,如果失败,执行 doAcquireShared,注意到 addWaiter 和 selfInterrupt 被放在了doAcquireShared 里面,也就是说基本都一样。

锁的获取方面,最大的区别有两个,一个是addWaiter 传入的节点不同,另一个是尝试成功后,独占锁是 setHead,共享锁是 setHeadAndPropagate,这是由于共享锁一个节点在开始处拿到锁,后面的节点也可能会拿到,所以尝试向后传播一下。

另外,独占锁和共享锁在获取和释放时都可能会唤醒后续节点,在独占锁中是 unparkSuccessor,共享锁是 unparkSuccessor 和 doReleaseShared。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

3 锁的释放 releaseShared

流程如下:尝试释放(tryReleaseShared),成功后继续唤醒后一个节点(doReleaseShared),并返回 true;失败则返回 false 。

和 release一样,这里只执行一次。而获取会反复尝试,直到成功。

    /**
     * Releases in shared mode.  Implemented by unblocking one or more
     * threads if {@link #tryReleaseShared} returns true.
     *
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryReleaseShared} but is otherwise uninterpreted
     *        and can represent anything you like.
     * @return the value returned from {@link #tryReleaseShared}
     */
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

3.1 doReleaseShared

在共享锁的获取内的 setHeadAndPropagate 和释放 releaseShared中都被用到,目的是唤醒 head 下一个节点。在实际调用中,可能会唤醒多个。

最后 break 退出的条件是 head 没有发生变化,如果 head 发生了变化,会继续循环处理 head,这称为“调用风暴”,可以见 逐行分析AQS源码(3)——共享锁的获取与释放 segmentfault.com/a/1190000016447307。

最后,还有一个问题,那就是 PROPAGATE到底有什么用,SIGNAL 标识对下一个节点的 park/unpark,而 PROPAGATE 好像只是个标识,很奇怪。

    /**
     * Release action for shared mode -- signals successor and ensures
     * propagation. (Note: For exclusive mode, release just amounts
     * to calling unparkSuccessor of head if it needs signal.)
     */
    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;
              	// 如果ws是SIGNAL,不只是要修改状态,还要unpark
                if (ws == Node.SIGNAL) {
                  	// 如果CAS失败需要下一次循环
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
              	// 如果ws是0,尝试将其修改为 PROPAGATE。
              	// 如果cas需要下一次循环
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
          	// 如果 head 没变,退出循环,否则继续循环
          	// 如果继续循环,可能会多 unpark 几个节点
            if (h == head)                   // loop if head changed
                break;
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值