百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

AQS(二)共享锁(基于JDK 8) aqs如何实现共享锁

liebian365 2024-11-12 13:10 9 浏览 0 评论

文章目录

  • 1 介绍
  • 2 锁的获取 acquireShared
    • 2.1 doAcquireShared
    • 2.2 setHeadAndPropagate
    • 2.3 和独占锁的比较
  • 3 锁的释放 releaseShared
    • 3.1 doReleaseShared

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;
        }
    }

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: