JAVA并发(1)-AQS(亿点细节)

AQS(AbstractQueuedSynchronizer), 可以说的夸张点,并发包中的几乎所有类都是基于AQS的。

一起揭开AQS的面纱

1. 介绍

为依赖 FIFO阻塞队列阻塞锁相关同步器(semaphores, events等)的实现提供一个框架。 为那些依赖于原子state的同步器提供基础(CyclicBarrier、CountDownLatch等). 支持独占模式共享模式, 不同的模式需要实现不同的方法.

引用这位大佬的图 http://www.cnblogs.com/waterystone/p/4920797.html
AQS整体结构
这个图是AQS整体结构,从图中可以看到,AQS维护着一个阻塞队列(当线程获取资源失败时,就会进入该队列,等待,直到被唤醒), state是一个共享的资源。

2. 源码剖析

我们先看看AQS的类图,
AQS类图

内部类: Node,阻塞队列维护的元素;ConditionObject, 支持独占模式下的子类用作Condition实现, 后面会讲到。先看看Node的结构。

static final class Node {
	/** 表明阻塞一个共享模式的结点 */
        static final Node SHARED = new Node();
        /** 表明阻塞一个独占模式的结点 */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
	// 发生的情况: 中断或者超时
        static final int CANCELLED = 1;

        /** 表明后继节点需要被唤醒 */
	// 发生的情况: 后继结点被park()阻塞,当目前的结点释放或取消,必须要unpark它的后继结点
        static final int SIGNAL = -1;

        /** waitStatus value to indicate thread is waiting on condition */
	// 发生的情况: 当前结点在条件队列中(后面会讲解)
        static final int CONDITION = -2;

        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        // 发生的情况: 执行releaseShared后,需要传播释放其他结点。
        // 在doReleaseShared中进行设置。(后面讲解这个状态的必要性)
        // 在共享模式下,才会用到
        static final int PROPAGATE = -3;

	/** 结点的状态, 初始值为0*/
	volatile int waitStatus;

	volatile Node prev;

	volatile Node next;

	volatile Thread thread;
	 ....
}

tip: waitStatus > 0, 即 CANCELLED, 此时的结点不正常。

AQS使用了模板模式, 自主选择重新定义以下方法

  • tryAcquire - 独占模式
  • tryRelease - 独占模式
  • tryAcquireShared - 共享模式
  • tryReleaseShared - 共享模式
  • isHeldExclusively

调用这些方法,都会引发UnsupportedOperationException,后面的文章将通过其子类,来讲解它们的实现。

有了这些知识后,我们从下面这几个关键的共有方法入手去讲解AQS

  • acquire(int arg) - 独占模式
  • release(int arg) - 独占模式
  • acquireShared(int arg) - 共享模式
  • releaseShared(int arg) - 共享模式

2.1 acquire

独占模式下的获取资源,忽略中断。调用tryAcquire至少一次,若成功就返回。否则,将线程入队,并可能反复阻塞和接触阻塞,并调用tryAcquire直至成功。此方法可以用于实现 Lock.lock

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

tryAcquire在后面文章结合子类进行分析。
代码(2.1.1)中,此时调用了tryAcquire,获取资源失败,返回false,继续执行后续方法。

addWaiter -- Queuing utilities

使用当前线程和给定mode, 新建一个Node,并且将新建Node入队

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
           // (2.1.2) 记住这个地方, 后面有个知识点会用到
            node.prev = pred;
           // (2.1.3)
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 插入node入队,初始化如果有必要
        // 执行到这的情况:
        // 1. 阻塞队列为空时,即tail == null
        // 2. 代码(2.1.3), CAS失败
        enq(node);
        return node;
    }

enq -- Queuing utilities

一直循环直到node入队成功

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 初始时,head与tail都为空,需要新建一个哨兵结点
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            // 插入结点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

经过了上面的操作,目前的线程已经加入了队尾,此时做的事情就是阻塞自己,等待资源释放并且获取,然后执行自己的操作

acquireQueued

以独占且不可中断的模式,获取已经在阻塞队列中的线程。若在等待时被中断,返回true

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 不断自旋直到获取到资源或被park
            for (;;) {
                final Node p = node.predecessor();
                // 若node的前继结点是head,执行tryAcquire,尝试获取资源(可能刚好释放了资源,就可以不用阻塞)
                // (2.1.4)
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 检测是否需要被park,若需要就进行park,并返回在等待时是否被中断
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //  失败,发生的情况 1. 被中断 2. 超时.(其他方法调用该方法时,会发生)
            if (failed)
                cancelAcquire(node);
        }
    }

shouldParkAfterFailedAcquire -- Utilities for various versions of acquire

对获取资源失败的node,检测并获取结点。返回true,如果线程需要阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 前驱结点的状态为SIGNAL,此时就可以安心的park,等待前驱节点释放资源,然后唤醒自己
        if (ws == Node.SIGNAL)
            return true;

        // 此时ws为CANCELLED
        if (ws > 0) {
            /*
             * 前驱结点状态为CANCELLED,跳过前驱结点并重试, 直到前驱节点不为CANCELLED
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus此时的值要么是0要么是PROPAGATE.
             * 需要把前驱结点状态设置为SIGNAL
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

cancelAcquire -- Utilities for various versions of acquire

取消正在进行的获取资源的尝试

    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // 获取过滤后的前驱的后驱节点
        Node predNext = pred.next;

        node.waitStatus = Node.CANCELLED;

        // node为tail,就直接从队列中删除
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;

            // 1. 前驱结点不是头结点,该结点不是老二
            // 2. 满足以下任意一个条件:
                  // 2.1 此时的前驱结点状态为SIGNAL
                  // 2.2 此时的前驱结点状态为PROPAGATE或CONDITION,成功将状态设置为SIGNAL
            // 3. 前驱节点的任务不为空
            // 满足上面的条件,就将节点的前驱结点的next 指向 节点的后驱结点
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                  pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 此时结点刚好是老二;
                // 代码(2.1.4) 可以看出,头结点要么是哨兵结点,要么是已经获取到资源的结点。
                // 此时唤醒node的后驱结点,是为了防止后驱结点一直阻塞
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

unparkSuccessor

后驱结点存在一个在正在等待的结点,则唤醒它

private void unparkSuccessor(Node node) {

        int ws = node.waitStatus;

        // 将当前的结点的waitStatus设置为0,失去SIGNAL、PROPAGATE的含义
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        // (2.1.5)
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // [问题一] 为什么node的后驱结点为空,重新寻找是从后往前找
            // 只要waitStatus <= 0, 都有机会被唤醒
            for (Node t = tail; t != null && t != node; t = t.prev)
                // (2.1.6)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

2.1.1 acquire流程图

根据上面的分析,整一个流程图
acquire流程图

2.1.2 小结

根据acquire流程图,一句话小结其流程,尝试获取资源,失败则将新建node(当前线程及独占模式)入队,检测自己是否是老二,是老二就再一次尝试获取资源,成功就返回中断标志,不是老二就设置为SIGNAL,park自己,然后安心等待被唤醒。

2.2 release

独占模式下的释放资源。解除阻塞一个或多个线程,当tryRelease返回true时。此方法可以用于实现 Lock.unlock

    public final boolean release(int arg) {
        // tryRelease返回true,继续下面操作。
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

2.2.1 release流程图

因为release(int arg)的主要流程是在tryReleaseunparkSuccessor中,但是tryRelease又是在子类中实现,所以该流程图也可以看作unparkSuccessor的流程图
release流程图

2.2.2 小结

根据release流程图, 一句话小结其流程, 释放资源,唤醒后驱没有被取消的结点。

下面讲讲AQS的另一种模式,共享模式

2.3 acquireShared

共享模式下获取资源,忽略中断。至少调用tryAcquireShared一次,成功就返回。否则,线程将入队,可能会重复的阻塞和解除阻塞,直到调用tryAcquireShared成功。成功获取到资源,将会唤醒后驱结点,若资源满足。

    public final void acquireShared(int arg) {
        // 在子类探究tryAcquireShared
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

对tryAcquireShared返回的参数,进行简单的介绍

  • 返回负数表示失败;
  • 返回零,随后的线程都不能获取到资源
  • 返回正数,随后的线程可以获取到资源

此时tryAcquireShared的返回值是小于零,表示获取资源失败,进行下一步处理

doAcquireShared

获取资源在不可中断的模式下

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    // 若前驱结点是头结点且刚好释放了,尝试获取资源
                    // (2.3.1)
                    if (r >= 0) {
                        // 将当前node设置为head,并且尝试唤醒node的后驱结点
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 跟独占模式的处理是一样的
                // (2.3.2)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

setHeadAndPropagate

设置队列的头结点,达到条件就唤醒后面的结点.

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.
         */

        // tips: h == null、(h = head) == null、s == null,是为了防止空指针的发生

        // 执行到下面的情况:
        // 1. propagate是tryAcquireShared返回的值。propagate(资源) > 0, 表示还有资源可以唤醒后面的结点。
        // 否则,此时propagate = 0, 结合代码(2.3.1)
        // 2. 旧的head的waitStatus < 0
        // 旧的头结点释放了资源,执行了代码(2.3.4), 此时的waitStatus 为PROPAGATE(初始化为0)
        // 3. 此时的head已经是当前结点了,后面若有结点(此时后面的结点在park),
        // 那么新head的waitStatus肯定为SIGNAL, 结合代码(2.3.2)

        // 情况3,有可能会发生没必要的唤醒,因为此时去唤醒新head的后驱结点,但是此时还没
        // 有释放资源,它后驱结点唤醒后,去获取资源,获取失败,又被park.
        // 源码注释说到,虽然有争议,但是大多数情况下,需要去唤醒
        // (2.3.3)
        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.3)中, 为什么不只用propagate来判断是否唤醒后驱结点 [问题二]

doReleaseShared

共享模式下主要的释放资源的逻辑,唤醒后驱结点,确保线程不被挂起

 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) {
                    // (2.3.4)
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                // (2.3.5)
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

2.3.1 acquireShared的流程图

image

2.3.2 小结

一句话小结acquireShared的流程,尝试获取资源,若获取到资源,资源还有剩余就去继续唤醒后驱结点,若尝试获取资源失败,就park自己,等待被唤醒。 跟acquire相比,最大的区别就是,获取到资源acquireShared,还会去尝试唤醒其后驱结点

2.4 releaseShared

Releases in shared mode. Implemented by unblocking one or more threads if tryReleaseShared returns true.

    // 代码比较简单,就不分析了~
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

2.5 问题解答

[问题一] 为什么在唤醒后驱结点时,node的后驱结点为空,需要重新从后往前找

   // (2.1.2) 记住这个地方, 后面有个知识点会用到
   node.prev = pred; // 1
   // (2.1.3)
   if (compareAndSetTail(pred, node)) { // 2
      pred.next = node; // 3
      return node;
   }

仔细观察代码(2.1.2) 和(2.1.3), 此时添加结点相当于有三步,都不是原子性的,当执行到第二步时,就要唤醒后驱结点了,此时新增的结点只设置了前驱结点,队列设置了尾结点,但是没有设置后驱结点,如果从前往后查找的话,可能会丢失符合要求的结点。

[问题二],代码(2.3.3)中, 为什么不只用propagate来判断是否唤醒后驱结点。
请看这位大佬的博客 讲的非常详细
大致意思就是,
我们假设有A、B、C、D四个线程,前两个释放资源的线程,后两个是争抢资源的线程,此时只有A或B释放了资源,C、D才可以被唤醒,假设我们不看PROPAGTE
时刻一: A线程释放资源,执行代码 (2.3.4),head的waitStatus从SIGNAL(-1)变为了0
时刻二: C线程获取到资源,执行到代码(2.3.1), tryAcquireShared返回0
时刻三: B线程线程释放资源,执行代码 (2.3.4),因为此时未改变头结点,head的waitStatus为0,不能unparkSuccessor
时刻四: 此时C执行到代码(2.3.3),propagate(tryAcquireShared返回值)为0,C也不会去唤醒后驱结点,D线程就永远GG了

引用doReleaseShared注释中的一句话

status is set to PROPAGATE to ensure that upon release, propagation continues.

2.6 Condition

使用synchronized时,线程间通信使用wait, notify and notifyAll;而使用AQS实现的lock,线程间的通信就使用Condition中的await、signal...。Condition与Lock结合使用,同一个lock对应多个Condition。

sync queue & condition queue

public class ConditionObject implements Condition...

在AQS中,已经对Condition的方法进行了实现,子类想使用的话,只需要调用ConditionObject就行了

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

本来想跟着源码走,简简单单介绍一下Condition,但是源码有几处细节,让我头秃,在网上搜索别人的博客,这篇博客解开了我的疑惑,对Condition介绍的非常详细,写的非常的完美~

根据大佬的博客,那我们下面简单讲解Condition的两个常用方法

  • await
  • signal

2.6.1 await & signal

导致目前线程阻塞直到被唤醒或中断;调用await后,会将当前的线程封装成node,加入到条件队列

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 添加结点到条件队列中
            Node node = addConditionWaiter();
            // 释放当前线程持有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 不在同步队列中,就park
            // (2.6.1)
            while (!isOnSyncQueue(node)) {
                // 执行到这,当前线程会立即挂起
                LockSupport.park(this);
                // 运行到这的话,情况: 1. signal 2. 中断
                // 检验中断
                // (2.6.2)
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 第二部分
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

为了讲清楚代码(2.6.1)之后的逻辑,我们先看看signal的源码

将condition queue中等待最长的结点转移到sync queue中去,去争抢资源

        public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

此时,执行signal的主要逻辑

        private void doSignal(Node first) {
            do {
                if ( (firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                // 将后驱结点置空
                // (2.6.3)
                first.nextWaiter = null;
            } while (!transferForSignal(first) &&
                     (first = firstWaiter) != null);
        }

将condition queue中的一个结点转移到sync queue中去

    final boolean transferForSignal(Node node) {
        // 这里表示当前已经被取消了。
        // (2.6.4)
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        // 将当前结点放入sync queue的末尾, 此时返回的是当前结点的前驱结点(一定要注意)
        Node p = enq(node);
        int ws = p.waitStatus;
        // 前驱结点被取消,或者设置SIGNAL状态失败,就直接唤醒当前线程, 唤醒 = 有资格去竞争锁了
        // enq返回的是前驱结点,我人傻了,看成是返回当前线程,就一直觉得逻辑不对。
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

这里signal的逻辑就讲完了,总结一下:

  1. condition queue中找出等待时间最长且未被取消的结点, 转移到sync queue的队尾去,同时要在condition queue中删除该结点
  2. 若在sync queue中的该结点的前驱结点被取消了或设置SIGNAL状态失败,要直接唤醒它,叫它去竞争锁。

signalAll的主要逻辑和signal是一样的,差别就是signalAll会把所有在condition queue中的结点转移到sync queue中去,并清空所有在condition queue中的结点,下面只贴一下signalAll的主要代码,

        private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

我们再次回到await中去

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 添加结点到条件队列中
            Node node = addConditionWaiter();
            // 释放当前线程持有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 不在同步队列中,就park
            // (2.6.1)
            while (!isOnSyncQueue(node)) {
                // 执行到这,当前线程会立即挂起
                LockSupport.park(this);
                // 运行到这的话,情况: 1. signal 2. 中断
                // 检验中断
                // (2.6.2)
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 第二部分
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

代码(2.6.1),继续讲解
isOnSyncQueue

一开始在条件队列中,现在在sync queue中等待重新获取资源,如果有这种的node就返回true

    final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /*
         * node.prev can be non-null, but not yet on queue because
         * the CAS to place it on queue can fail. So we have to
         * traverse from tail to make sure it actually made it.  It
         * will always be near the tail in calls to this method, and
         * unless the CAS failed (which is unlikely), it will be
         * there, so we hardly ever traverse much.
         */
        return findNodeFromTail(node);
    }

findNodeFromTail

从尾部找寻结点

    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            // 结合下面的enq代码以及图思考一下,会明白此方法的意义的
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

node.waitStatus == Node.CONDITION, 表示当前结点肯定在condition queue中。

为何是上面的那些条件?
我们上面看了转移到sync queue是用的enq方法

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // (2.6.5)
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

结合代码(2.6.5),思考一下就知道isOnSyncQueue中条件设置的道理了,但是为何需要findNodeFromTail啦? 这是需要补充一个知识点了,在多个线程执行enq时,只有一个线程会设置为tail成功,其余的都只是设置prev,就可能会出现下面图片中的情况,'多个尾巴'一直不断自旋,最后会变成一个正常的链表。
enq造成的多个尾巴
此时线程的状态是,调用await后,将结点添加到条件队列中,且释放了自己持有的所有资源,并将自己park,此时等待被signal或者被中断。

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 添加结点到条件队列中
            Node node = addConditionWaiter();
            // 释放当前线程持有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 不在同步队列中,就park
            // (2.6.1)
            while (!isOnSyncQueue(node)) {
                // 执行到这,当前线程会立即挂起
                LockSupport.park(this);             ///  我们在这被挂起了
                // 运行到这的话,情况: 1. signal 2. 中断
                // 检验中断
                // (2.6.2)
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 第二部分
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

执行到代码(2.6.2), 我们直到可能是被signal或被中断了。现在要解决的是,

  1. 是否被中断?
  2. 何时被中断?
  3. 中断如何处理?

我们带着这三个问题,继续出发~

补充一个小知识点,AQS定义了三种情况中断的值

  • THROW_IE, signal前被中断,要抛出InterruptedException
  • REINTERRUPT, signal后被中断
  • 0, 未被中断

关于REINTERRUPT这个中断,可以理解成,吃饭,吃完了但是还有一个菜没有上,问服务员,“如果没有炒,就不要了”,但是服务员告诉,菜已经下锅,所以这时候的中断就是REINTERRUPT,中断的太晚了。 -- 例子来自上面的那篇博客

我们继续看向代码(2.6.2)
checkInterruptWhileWaiting

        private int checkInterruptWhileWaiting(Node node) {
            return Thread.interrupted() ?
                (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
                0;
        }

若被中断Thread.interrupted肯定为true

transferAfterCancelledWait

    final boolean transferAfterCancelledWait(Node node) {
        // 中断情况一: 此时结点还在condition queue中,肯定是signal前就被中断了
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            // 这里还要加入到sync queue中,获取到锁才能抛出错,继续往后看
            enq(node);
            return true;
        }
        /*
         * If we lost out to a signal(), then we can't proceed
         * until it finishes its enq().  Cancelling during an
         * incomplete transfer is both rare and transient, so just
         * spin.
         */
         // 中断情况二: 这里的情况就是signal后,node还没有执行enq,毕竟执行signal到执行enq还有几个步骤
         // 此时就自旋,等待node转移到sync node中就行了
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

上面的代码已经注明了,各种情况的发生时机,此时我们来到了await的第二部分~

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            // 添加结点到条件队列中
            Node node = addConditionWaiter();
            // 释放当前线程持有的锁
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            // 不在同步队列中,就park
            // (2.6.1)
            while (!isOnSyncQueue(node)) {
                // 执行到这,当前线程会立即挂起
                LockSupport.park(this);             ///  我们在这被挂起了
                // 运行到这的话,情况: 1. signal 2. 中断
                // 检验中断
                // (2.6.2)
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 第二部分
            // acquireQueued获取到锁,并返回是否被中断。
            // 有一种情况需要提一下,acquireQueued返回true,上面的interruptMode = 0,
            // 表示signal后,在获取锁的时候被中断了
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;

            // node还在condition上,说明是被取消了的node,清除它
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                // 对上面得到的interruptMode做出回应
                reportInterruptAfterWait(interruptMode);
        }

reportInterruptAfterWait

        private void reportInterruptAfterWait(int interruptMode)
            throws InterruptedException {
            if (interruptMode == THROW_IE)
                throw new InterruptedException();
            else if (interruptMode == REINTERRUPT)
                selfInterrupt();
        }

2.6.2 await与signal的流程图

await流程图


signal流程图

2.6.3 小结

先讲讲await的流程,看起流程图有点吓人,其实很多步骤是对不同时机的中断操作的记录
当await被执行,下面简单总结下await的流程

  1. 当前线程与CONDITION状态封装成node,加入到condition queue的末尾
  2. 释放线程之前获取的所有资源
  3. 若不在sync queue中,阻塞自己,等待被signal被中断
  4. 获取中断操作的时机,并记录表示何时中断的值(interruptMode)
  5. 不管是怎么被唤醒的,都要去竞争资源,直到获得资源为止
  6. 最后对不同的中断值,作出不同的操作

signal的流程就相对于简单一点

  1. 获取condition queue的头结点
  2. 检验是否被取消,若被取消,就获取头结点的后驱结点,以此类推;
  3. 将结点从condition queue中转移到sync queue中,而且会从condition queue中删除该节点
  4. 若结点插入sync queue,得到的前驱结点,被取消了,或者CAS前驱结点状态为SIGNAL
    失败,将直接unpark当前线程

3. 总结

Doug Lea,太秀了。AQS中有很多细枝末节的东西,只有自己去读了源码,理解为何这样做,你才会明白才会真正读懂AQS。
关于学习和写AQS文章时,看了一些博客,为我解答了自己的疑惑,慢慢加油,我也要向这些大佬看齐~

4. 参考

~~# 5. 面试中问题
这是我的一个想法,若我博客中写过的知识,在面试中有问到过,我会记录下来,没有就是目前还没遇到过

posted on 2021-05-21 15:35  ukyu  阅读(496)  评论(1编辑  收藏  举报

导航