关于aqs:AQSAbstractQueuedSynchronizer-源码分析

40次阅读

共计 14278 个字符,预计需要花费 36 分钟才能阅读完成。

AQS(AbstractQueuedSynchronizer) 源码剖析

阐明

  1. 本文基于 jdk 8 写作。
  2. @author JellyfishMIX – github / blog.jellyfishmix.com
  3. LICENSE GPL-2.0

AQS 须要关注的点有哪些?

  • AQS 全称 AbstractQueuedSynchronizer,是 juc 包 (java.util.concurrent) 中一个同步器开发框架,用于反对下层的锁。
  • 要害的属性:state 同步状态,CHL 同步队列。
  • 两种模式:独占模式,共享模式。

AQS 的要害属性

  1. sate 同步状态,是锁的表述,示意有多少线程获取了锁。
  2. CHL 队列(同步队列),由链表实现,以 CAS + 自旋的形式获取资源,是可阻塞的先进先出的双向队列。通过 CAS + 自旋保障节点插入和移除的原子性。当有线程获取锁失败,就被增加到队列开端。当有线程开释了锁,会从 CHL 队头出队一个线程。

state 同步状态

/**
 * The synchronization state.
 */
private volatile int state;

AQS 锁反对的子类,能够借助操作同步状态 state 来实现尝试获取同步状态 (tryAcquire)、尝试开释同步状态(tryRelease) 的逻辑。

也就是说,AQS 的子类 (各种锁) 判断何时能获取同步状态,何时能开释同步状态,须要借助于判断 state 的值。而后能把获取同步状态进一步封装成获取锁(lock),把开释同步状态封装成开释锁(unlock)。至于 state 为何值时能够获取同步状态,state 为何值时能够开释同步状态,这些都是交由子类判断的。

AQS 只是当子类判断为未获取到同步状态时,通过 CHL 入队操作,把线程退出到同步队列中,而后让线程休眠。当子类判断为能够开释同步状态时,从 CHL 同步队列中唤醒一个线程。

CHL 队列

在剖析前,可能会有如下纳闷:

  1. 节点的数据结构是什么样的?
  2. 是单向还是双向?
  3. 有无头节点和尾节点?

Node

CHL 是链表,链表是由节点 node 组成的。

    static final class Node {
        /**
         * 标记一个节点正在共享模式下期待
         */
        static final Node SHARED = new Node();
        /**
         * 标记一个节点正在独占模式下期待
         */
        static final Node EXCLUSIVE = null;

        /**
         * waitStatus value,标记以后节点的线程被 cancel 勾销
         */
        static final int CANCELLED =  1;
        /**
         * waitStatus value,标记后继节点的线程须要被唤醒
         */
        static final int SIGNAL    = -1;
        /**
         * waitStatus value,标记以后节点的线程进入期待队列中
         */
        static final int CONDITION = -2;
        /**
         * waitStatus value,示意下一次共享式的同步状态获取将会无条件流传上来
         */
        static final int PROPAGATE = -3;

        /**
         * 节点的状态
         */
        volatile int waitStatus;

        /**
         * 以后节点的前驱节点
         */
        volatile Node prev;

        /**
         * 以后节点的后继节点
         */
        volatile Node next;

        /**
         * 以后节点援用的线程
         */
        volatile Thread thread;

        /**
         * 期待队列中的下一个节点
         */
        Node nextWaiter;
    }

留神到属性 prev, next, 阐明 CHL 是一个双向队列。

AQS 中有两个属性,用作头尾指针,通过头尾指针来治理同步队列。头尾指针应用场景包含获取锁失败的线程进行入队,开释锁时对同步队列中的线程进行告诉等外围办法。


    /**
     * 指向 CHL 队列的头节点
     */
    private transient volatile Node head;

    /**
     * 指向 CHL 队列的尾节点
     */
    private transient volatile Node tail;

CHL 队列中 node 的出队和入队操作,实际上对应着同步状态的获取和开释两个操作:获取同步状态失败 -> 进行入队操作,获取同步状态胜利 -> 进行出队操作。

独占模式与共享模式

  1. AQS 的工作模式分为独占模式 (EXCLUSIVE) 和共享模式(SHARED),记录在 node 的信息中。
  2. 独占模式:同一时间只有一个线程能拿到锁执行,锁的 state 只有 0 和 1 两种状况(ReentrantLock 的可重入锁是个例外)。典型的实现如 ReentrantLock。
  3. 共享模式:同一时间有多个线程能够拿到锁执行,锁的 state 大于或等于 0。典型的实现如 CountDownLunch。
  4. 它的实现类 (子类) 中,要么实现并应用了它独占性能的 API,要么应用了共享性能的 API,而不会同时应用两套 API。即使是它有名的子类 ReentrantReadWriteLock,也是通过两个外部类:读锁和写锁,别离实现的两套 API 来实现的。
  5. AQS 应用了模板办法设计模式,定义一套操作逻辑,相当于一个算法的骨架,而将一些步骤的实现提早到子类中,比方获取、开释 state 同步状态。

独占模式

以下是独占模式的办法

/* 独占式获取同步状态,如果获取失败则插入同步队列进行期待 */
void acquire(int arg);
/* 与 acquire 办法雷同,但在同步队列中进行期待的时候能够检测中断 */
void acquireInterruptibly(int arg);
/* 在 acquireInterruptibly 根底上减少了超时期待性能,在超时工夫内没有取得同步状态返回 false */
boolean tryAcquireNanos(int arg, long nanosTimeout);
/* 开释同步状态,该办法会唤醒在同步队列中的下一个节点 */
boolean release(int arg);
acquire 办法(独占模式获取同步状态)
    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * 模板办法,封装了线程获取资源失败后,进入同步队列并阻塞的逻辑
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        // tryAcquire 办法由子类实现,尝试获取同步状态
        if (!tryAcquire(arg) &&
            // 未获取到同步状态,则进入同步队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果在同步队列中产生中断,则中断标记为 true,执行 selfInterrupt() 办法
            selfInterrupt();}

咱们把 acquire 办法概括一下:

// 交由子类判断是否能胜利获取到同步状态
if (!tryAcquire(arg) &&
            // 未获取到同步状态,则调用 addWaiter 办法把线程构建成 node,追加到 CHL 同步队列尾部。// 追加至 CHL 同步队列后,调用 acquireQueued 办法让线程休眠,此办法也蕴含了线程昏迷后从新抢同步状态的逻辑。acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果在休眠过程中产生中断,则中断标记为 true,执行 selfInterrupt() 办法
            selfInterrupt();
  1. 先调用 tryAcquire 办法(由子类实现,获取同步状态),判断是否获取胜利。
  2. 如果获取胜利,acquire 办法返回完结。如果获取失败,调用 addWaiter 办法,把以后线程构建成 node,把 node 追加至 CHL 同步队列的尾部。
  3. node 追加至 CHL 队列尾部后,调用 acquireQueued 办法,把以后线程休眠。同时 acquireQueued 外面也包含了线程休眠被唤醒后尝试抢锁的逻辑。

acquire 办法流程图:

addWaiter 办法
    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * 依据传入的独占 / 共享模式,在 CHL 队列中增加一个 node
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        // 依据传入的独占 / 共享模式,结构一个新 node
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        /*
         * 上面这个 if 分支里在做的事件,和 enq 办法中在做的事件雷同。能够了解为,把 enq 中的局部代码放在了里面。* 这种做法在并发编程中还挺常见,把最有可能胜利执行的代码间接写在最罕用的调用处。* 在线程数不多的状况下,CAS 还是很难失败的,因而这种写法能够节俭多条指令。比方节俭了办法调用、enq 办法中进入循环的开销。* 是 jdk 中一种极致的优化,业务代码能够不必做到这么极致。*/
        Node pred = tail;
        // 如果队尾不为空(即以后队列中有元素),则尝试在队尾追加 node
        if (pred != null) {
            /*
             * node.prev = pred 这里其实是有线程平安问题的,并发状况下可能有多个刚结构进去的 node.prev 被设置为了 pred。* 然而没关系,前面 CAS 设置队尾的时候会失败,而后进入 enq 办法中循环去做。当某次 CAS 胜利时,阐明 CAS 前的那步 node.prev 赋值也正确了。* 所以,这里只是部分存在线程平安问题,最终后果是平安的。* 为什么设置尾节点时都要先将之前的尾节点设置为 node.prev 的值呢,而不是在 CAS 之后再设置?* 因为如果不提前设置 node.prev 的话,在 CAS 设置完 tail 后会存在一瞬间的 tail.pre 为 null 的状况,而 Doug Lea 正是思考到这种状况,不管何时获取 tail.pre 都不会为 null
            */
            node.prev = pred;
            // CAS 尝试设置尾节点为以后节点
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 尝试循环在队尾追加 node
        enq(node);
        return node;
    }
enq 办法
    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     *
     * 尝试循环在队尾追加 node
     *
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {for (;;) {
            Node t = tail;
            // 如果 tail 为空,阐明队列中还没有元素,须要初始化一个头节点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                /*
                 * node.prev = t 这里其实是有线程平安问题的,并发状况下可能有多个刚结构进去的 node.prev 被设置为了 t。* 然而没关系,下一步 CAS 设置队尾的时候会失败,而后进入下一次循环。当某次 CAS 胜利时,阐明 CAS 前的那步 node.prev 赋值也正确了。* 所以,这里只是部分存在线程平安问题,最终后果是平安的。* 为什么设置尾节点时都要先将之前的尾节点设置为 node.prev 的值呢,而不是在 CAS 之后再设置?* 因为如果不提前设置 node.prev 的话,在 CAS 设置完 tail 后会存在一瞬间的 tail.pre 为 null 的状况,而 Doug Lea 正是思考到这种状况,不管何时获取 tail.pre 都不会为 null
                 */
                node.prev = t;
                // CAS 尝试设置尾节点为以后节点
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

addWaiter 办法的逻辑:把以后线程构建成 node,把 node 追加至 CHL 同步队列的尾部。粗略地了解,咱们能够把 enq 办法合并至 addWaiter 办法中,enq 办法在承当 addWaiter 办法的一部分逻辑。咱们把次要逻辑概括一下:

// 依据传入的独占 / 共享模式,结构一个新 node
for (;;) {
    // CAS 操作尝试向 CHL 队列尾部追加此 node。CAS 操作胜利向队尾追加后,跳出循环。// CAS 操作如果失败,则进行下一次的循环。}

咱们留神到,addWaiter 办法 和 enq 办法中,有局部代码做的事件雷同。能够了解为,把 enq 中的局部代码放在了里面。这种做法在并发编程中还挺常见,把最有可能胜利执行的代码间接写在最罕用的调用处。在线程数不多的状况下,CAS 还是很难失败的,因而这种写法能够节俭多条指令。比方节俭了办法调用、enq 办法中进入循环的开销。这种写法是 jdk 中一种极致的优化,业务代码能够不必做到这么极致。

咱们还能够留神到 node.prev = t; 放在了 if CAS 之前,这里其实是有线程平安问题的,并发状况下可能有多个刚结构进去的 node.prev 被设置为了 t。然而没关系,下一步 CAS 设置队尾的时候会失败,而后进入下一次循环。当某次 CAS 胜利时,阐明 CAS 前的那步 node.prev 赋值也正确了。所以,这里只是部分存在线程平安问题,最终后果是平安的。

为什么设置尾节点时都要先将之前的尾节点设置为 node.prev 的值呢,而不是在 CAS 之后再设置?因为如果不提前设置 node.prev 的话,在 CAS 设置完 tail 后会存在一瞬间的 tail.pre 为 null 的状况,而 Doug Lea 正是思考到这种状况,不管何时获取 tail.pre 都不会为 null

acquireQueued 办法

咱们回到 acquire 办法中,去看 acquireQueued 办法。

    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     *
     * 在 CHL 队列中的线程获取同步状态的逻辑,是 CHL 队列中的线程获取同步状态 / 睡眠的外围逻辑
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        // 获取同步状态是否胜利
        boolean failed = true;
        try {
            // 中断标记
            boolean interrupted = false;
            // 循环尝试获取同步状态
            for (;;) {
                // 以后节点的前驱节点
                final Node p = node.predecessor();
                // 判断以后节点的前驱节点是不是头节点,如果是则阐明应该出队了,调用 tryAcquire 办法(子类实现),获取同步状态
                if (p == head && tryAcquire(arg)) {
                    // 同步状态获取胜利,把以后节点设置为头节点
                    setHead(node);
                    // 把老的头节点开释掉(通过去除老头节点的援用关系,最终让 GC 去回收此对象)p.next = null; // help GC
                    failed = false;
                    // 返回中断标记
                    return interrupted;
                }
                // 如果走到这一步,阐明以后节点不是头节点,或未获取到同步状态,阐明获取同步状态失败(这种状况存在于其余线程曾经领先获取了同步状态)。判断是否须要休眠。if (shouldParkAfterFailedAcquire(p, node) &&
                    // 进入睡眠状态,判断在睡眠状态中是否被中断
                    parkAndCheckInterrupt())
                    // 如果在睡眠状态中被中断,则设置中断标记为 true
                    interrupted = true;
            }
        } finally {
            // 比方以后节点因为超时或响应了中断,须要勾销本人
            if (failed)
                // 勾销获取同步状态
                cancelAcquire(node);
        }
    }

acquireQueued 办法是 CHL 队列中的线程获取同步状态 / 睡眠的外围逻辑。咱们把次要逻辑概括一下:

for (;;) {
    // 尝试获取同步状态,抢到同步状态则跳出循环
    // 未抢到同步状态,让线程休眠
}

在进入休眠前,尝试再获取一次同步状态,如果依然失败,则会调用 shouldParkAfterFailedAcquire 办法,判断是否须要休眠。

shouldParkAfterFailedAcquire 办法
    /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * 查看未获取到同步状态的 node 的状态,并更新 status。* 如果线程应该阻塞,返回 true。这是在所有循环获取同步状态中,次要的信号管制。要求 pred == node.prev
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        // 获取以后节点的前驱节点的状态
        int ws = pred.waitStatus;
        // 如果前驱节点的状态为 SIGNAL,示意前驱节点的后继节点(即以后节点)的线程须要被唤醒
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             *
             * 以后节点曾经把前驱节点设置为了 SIGNAL,示意以后节点须要被唤醒,所以它能够平安地阻塞(休眠)*/
            return true;
        // 如果前驱节点的状态 > 0(即 CANCELLED 状态)if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             *
             * 前驱节点被勾销(比方前驱节点曾经因为超时或响应了中断,而勾销了本人),跳过前驱节点至下一个前驱节点。*/
            do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);
            // 新的前驱节点把后继节点批改为以后节点
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             *
             * 在休眠前须要给前驱节点设置一个标识,将 waitStatus 设置为 Node.SIGNAL(-1)
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

在 shouldParkAfterFailedAcquire 办法中,须要过滤有效的前驱节点(比方曾经被勾销的前驱节点),而后把无效的前驱节点设置为 SIGNAL,示意前驱节点的下一个节点(即以后节点)须要被唤醒,而后它能够平安地阻塞(休眠)。

在之前的 acquireQueued 办法中,调用 shouldParkAfterFailedAcquire 办法判断是否须要休眠。如果须要休眠,则会调用 parkAndCheckInterrupt 办法,进行休眠。

parkAndCheckInterrupt 办法
    /**
     * Convenience method to park and then check if interrupted
     *
     * 以后线程进入休眠状态,返回值示意休眠过程中是否产生了中断
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        // 以后线程休眠
        LockSupport.park(this);
        // 重置线程的中断标记为 false,并返回一个 boolean 值示意线程的中断标记被重置前的值(true/false)
        return Thread.interrupted();}

如果在休眠过程中产生中断,中断标记会被设置为 true,而后持续在 acquireQueued 的自旋 for(;;) 中尝试获取同步状态,获取同步状态后,acquireQueued 办法返回。这也阐明了,即便线程在休眠过程中产生了中断,也只有在线程获取到同步状态后,能力响应中断。如果在休眠过程中没有产生中断,则中断标记仍然是 false。

acquireQueued 的返回值是一个中断标记,示意在休眠过程中是否产生中断。咱们再从新看一下 acquire 办法。

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * 模板办法,封装了线程获取资源失败后,进入同步队列并阻塞的逻辑
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        // tryAcquire 办法由子类实现,尝试获取同步状态
        if (!tryAcquire(arg) &&
            // 未获取到同步状态,则进入同步队列
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 如果在同步队列中产生中断,则中断标记为 true,执行 selfInterrupt() 办法
            selfInterrupt();}

如果在同步队列中产生中断,则中断标记为 true,执行 selfInterrupt() 办法。

selfInterrupt() 办法
    /**
     * Convenience method to interrupt current thread.
     *
     * 以后线程中断本人
     */
    static void selfInterrupt() {Thread.currentThread().interrupt();}

相当于在 acquire 办法中,让以后线程本人中断本人。咱们在 acquire 办法的调用处,或者更下层的调用处,就能进行中断的捕捉和解决。

acquireQueued 办法的诘问 — 为什么 acquire 办法要靠 acquireQueued 办法的返回值判断,来调用 .interrupt() 办法

这里的诘问探索的比拟粗疏,如果不感兴趣间接跳过即可,对了解次要逻辑影响不大。

acquireQueued 办法会返回一个中断标记(acquireQueued 办法中的局部变量 interrupted,以下简称为中断标记 interrupted),示意是否产生中断。acquireQueued 办法返回后,回到了 acquire 办法:

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

如果中断标记 interrupted 为 true,则会执行 selfInterrupt() 办法,也就是执行:

Thread.currentThread().interrupt();

为什么要做的这么麻烦呢,还要等 acquireQueued 办法返回之后,依据中断标记来做中断。咱们间接在 acquireQueued 办法中去执行 Thread.currentThread().interrupt(); 不行吗?

咱们来回顾一下 acquireQueued 的源码,尝试在 1 号地位 和 2 号地位加 Thread.currentThread().interrupt();,看看能不能防止应用 interrupted 这个返回值。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    // 2 号地位
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    // 1 号地位
                    interrupted = true;
            }
        } finally {if (failed)
                cancelAcquire(node);
        }
    }

1 号地位加 Thread.currentThread().interrupt(); 不能够。如果 .interrupt() 办法在 1 号地位被调用了,以后 Thread 的中断标记会被被置为 true。如果下一次循环中,线程没有获取到同步状态,会走让线程休眠的逻辑。而让线程休眠的办法 parkAndCheckInterrupt 调用的是 LockSupport.park

    private final boolean parkAndCheckInterrupt() {
        // 以后线程休眠
        LockSupport.park(this);
        // 重置线程的中断标记为 false,并返回一个 boolean 值示意线程的中断标记被重置前的值(true/false)
        return Thread.interrupted();}

LockSupport.park 办法会让线程进入休眠状态,然而以后 Thread 的中断标记为 true 时,LockSupport.park 会立即返回,不会产生阻塞。这就很危险了,如果线程始终无奈获取到同步状态,acquireQueued 办法中

for (;;) {
    // 尝试获取同步状态,抢到同步状态则跳出循环(办法返回)// 未抢到同步状态,让线程休眠
}

for (;;) 循环无奈休眠,会始终空转,对 cpu 性能耗费会非常重大。

这也是为什么在让未获取到同步状态的线程休眠时,调用的办法 parkAndCheckInterrupt 中,在 LockSupport.park 办法完结后,会调用 Thread.interrupted() 办法重置线程的中断标记。

这里插入一下对于线程的中断标记 (interrupt flag) 的介绍:

什么是 Thread 的中断标记(interrupt flag)?

中断标记(interrupt flag) 是线程产生中断时,设置的外部属性。在 Thread.java 中是看不到这个 interrupt flag 属性的,因为它写在了 c++ 的代码中,底层设置中断标记的办法 private native void interrupt0() 是一个 native 办法。咱们只须要晓得,中断标记(interrupt flag) 用来示意线程是否产生了中断。

怎么设置中断标记?

要设置一个线程的中断标记,只须要简略的在线程对象上调用 thread.interrupt() 办法。

怎么革除中断标记?

Thread.interrupted() 办法能够重置中断标记为 false,并返回一个 boolean 值示意中断标记被重置前的值(true/false)。

2 号地位加 Thread.currentThread().interrupt(); 也不能够。咱们看一下 AQS 中 acquireQueued 办法的调用处,能够看到有好几处中央用到了 acquireQueued 办法:

咱们选一个调用处来看看:

能够看到,在这里,依据 acquireQueued 办法的返回值(中断标记 interrupted),进行了别的一些解决,并没有间接调用Thread.currentThread().interrupt()。所以咱们在 acquireQueued 办法外部 2 号地位加 Thread.currentThread().interrupt() 去管制以后线程的中断标记 interrupt flag 也是不适合的。

至此,咱们解释了问题:acquireQueued 办法的诘问 — 为什么 acquire 办法要靠 acquireQueued 办法的返回值判断,来调用 .interrupt() 办法。

release 办法(独占模式开释同步状态)

release 是 AQS 独占模式开释同步状态的办法。

    /**
     * Releases in exclusive mode.  Implemented by unblocking one or
     * more threads if {@link #tryRelease} returns true.
     * This method can be used to implement method {@link Lock#unlock}.
     *
     * 模版办法,其中 tryRelease 办法由子类实现。以后锁开释指定的 state 数量,并返回锁是否开释胜利。*
     * @param arg the release argument.  This value is conveyed to
     *        {@link #tryRelease} but is otherwise uninterpreted and
     *        can represent anything you like.
     * @return the value returned from {@link #tryRelease}
     */
    public final boolean release(int arg) {
        // tryRelease 办法由子类实现,尝试开释同步状态
        if (tryRelease(arg)) {
            Node h = head;
            // 判断边界条件,而后把 CHL 同步队列中头节点的后继节点唤醒
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

这是一个模板办法,次要做了两件事件。一个是调用子类实现的办法去尝试开释同步状态。一个是把队列中头节点的后继节点唤醒。

AQS 唤醒 CHL 同步队列中的后续节点,调用了 unparkSuccessor 办法。

unparkSuccessor 办法
    /**
     * Wakes up node's successor, if one exists.
     * 
     * 如果以后节点的后继节点存在,则唤醒它
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        // node 的状态
        int ws = node.waitStatus;
        // 如果以后节点的状态不是被勾销,则把以后节点的状态置为 0,能够了解为重置以后节点的状态(之前的状态可能是用于批示后继节点的,比方 SIGNAL)。if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        // 以后节点的后继节点
        Node s = node.next;
        // 如果以后节点的后继节点为空,或者后继节点被勾销
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 则从尾节点开始从后向前遍历,找到最靠近以后节点的失常节点
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 以后节点的后继节点不为 null 时,唤醒以后节点的后继节点
        if (s != null)
            LockSupport.unpark(s.thread);
    }

unparkSuccessor 办法在做的事件是,如果以后节点的后继节点存在,则唤醒它。大部分代码在做边界条件的解决,真正唤醒线程应用的 LockSupport.unpark(thread) 办法。

LockSupport.unpark(thread) 这个办法咱们能够临时不必关注。它外面调用了 UNSAFE.unpark(thread),UNSAFE 类是一个很底层的类,并且是闭源的,不举荐一般开发人员在业务代码中间接调用的,个别是框架底层一些的实现会去应用。咱们只须要晓得,LockSupport.unpark(thread) 能够唤醒一个线程即可。

未完待续

本文目前仅剖析了 AQS 中,独占模式获取同步状态的逻辑,以下内容待欠缺:

  1. AQS 中共享模式获取同步状态的逻辑。
  2. AQS 中共享模式开释同步状态的逻辑。
  3. CHL 队列为什么是双向的?(剖析中曾经体现了,打算在文章中概括答复)

工夫无限,未完待续 …

正文完
 0