AQS 同步队列创建,入队,出队与取消

这是AQS 解读的第二篇,着重介绍同步队列的创建与操作;参见第一篇,同步队列在管程入口处;AQS构建的同步队列是一个双向链表;

AbstractQueuedSynchronizer中的属性

AQS中属性截图

	/**
     * Head of the wait queue, lazily initialized.  Except for
     * 等待队列的头节点,懒初始化。
     * initialization, it is modified only via method setHead.  Note:
     * 除了初始化,它只能通过 setHead 方法修改。
     * If head exists, its waitStatus is guaranteed not to be
     * 注意:如果头节点存在,它的状态不应该是取消的;
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * 等待队列的尾节点,懒初始化。
     * method enq to add new wait node.
     * 仅能通过方法 enq 添加一个新的等待节点而修改。
     */
    private transient volatile Node tail;

    /**
     * The synchronization state.
     * 同步状态
     * 该字段在MESA管程模型中表示共享变量
     */
    private volatile int state;

同步队列创建与入队

由于head和tail都是懒加载的,所以队列的创建是在入队时创建;

    /**
     * Creates and enqueues node for current thread and given mode.
     * 为当前线程和给定的模式创建节点并入队
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
    	// 以给定的 mode,创建当前线程对应的节点
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        // 尝试快速的路径入队;失败时在使用完整的enq方法入队
        Node pred = tail;
        // 如果尾节点存在,则构建一个双向的链表,前尾节点的next节点指向当前节点,当前节点的prev指向前尾节点;
        // 1,将当前节点的前置节点设置为尾节点
        // 2,cas设置尾节点为当前节点
        // 3,cas 成功,则将前尾节点的next指向当前节点
        if (pred != null) {
            node.prev = pred;
            // 4,cas 可能失败,失败表示别的线程优先完成了尾节点的设置
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 调用完整的入队方法
        enq(node);
        return node;
    }
   private Node enq(final Node node) {
        // 确保一定入队成功
        for (;;) {
        	// 获取尾节点
            Node t = tail;
            // 尾节点不存在,则cas设置一个头节点,cas成功,尾节点也指向头节点,cas失败,下一次循环时 tail节点就最终不为空;即使别的线程设置了头节点cas成功,但是未来及执行赋值tail操作;其余的线程将始终循环cas,且都以失败返回,直到那个幸运的线程开始重新争取到时间片完成后续的赋值操作
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            // 尾节点存在,则将当前节点的前置节点设置为尾节点;也许在addwaiter中做过这一步,但是cas失败了,所有进入完整的enq方法,这时能保证node.prev永远正确的指向当前最新的尾节点
                node.prev = t;
                // 直到将当前节点添加到队尾后结束循环,完成入队
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
入队总结

尾插法入队+双向链表;
头节点调用的Node 的无参构造器,队列初始化时该节点也是尾节点;当新的节点入队时,该尾节点也是头节点的next指向新来的节点,新来的节点的prev节点指向该节点;同时将指针指向新来的尾节点;
同步队列的新来节点入队前,总是先设置自己的prev,再cas抢占尾节点,最后再将前尾节点的next指向自己;
记住这个顺序很重要;某些时候是无法通过next指针正确找到下一个节点,但可以通过前置节点找到上一个节点;

为什么入队,以及入队后发生了什么?

回答这个问题的一个简单方式,就是调研哪些地方调用了入队方法;因为是在 addWaiter方法里调用enq,所以我们先研究哪些地方调用了 addWaiter方法。
acquire资源入队
我们将调用addWaiter方法的方法名列出来:
public final void acquire(int arg);
private void doAcquireInterruptibly(int arg)throws InterruptedException;
private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException ;
private void doAcquireShared(int arg) ;
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException;
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException;

可以很简单的看出,可以按照3种维度进行划分;
占用模式:独占获取或者共享获取
是否有时间限制:有时间限制获取和没有时间限制获取
是否响应中断:响应中断获取和不响应中断获取
时间限制以及是否响应中断的处理方式都很简单,这里我们不过多分析,因为本质上是很简单的api调用;这里我们按照是否独占来分析,选择简单独占模式 acquire 方法,以及简单共享模式doAcquireShared 方法分析

独占模式获取
    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * 独占模式下获取,忽略中断
     * by invoking at least once {@link #tryAcquire},
     * 以至少调用一次 tryAcquire 实现,
     * returning on success.  Otherwise the thread is queued, possibly
     * 仅在成功时返回。否则, 线程排队,
     * repeatedly blocking and unblocking, invoking {@link
     * 可能会重复性的阻塞与解开,直到调用 tryAcquire 成功
     * #tryAcquire} until success.  This method can be used
     * 这个方法可以用来实现lock
     * 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) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
	 /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * 以独占且忽略中断的方式为一个已经在同步队列中的线程进行获取;
     * queue. Used by condition wait methods as well as acquire.
     * 在acquire与条件 wait方法时调用
     * @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();
                // 仅当节点位于队首节点,才去尝试获取资源
                if (p == head && tryAcquire(arg)) {
                	// 出队
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 前置节点不是后置节点或者抢占资源失败,判断是否阻塞;如果需要阻塞,则阻塞线程
                if (shouldParkAfterFailedAcquire(p, node) &&
                	// 阻塞线程并在唤醒后检查是否设置中断标志
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
共享模式获取

与独占模式获取基本上大同小异,同样要入队,入队后判断是否可否获取资源,获取到资源则出队;无法获取资源则判断是否阻塞;

 /**
     * Acquires in shared uninterruptible mode.
     * @param arg the acquire argument
     */
    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);
                    if (r >= 0) {
                    	// 出队并传播
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

不同之处在于调用的子类实现的获取资源的方式不同,以及出队的方式不同;这里还有一个取消尝试获取中的节点,当发生错误时;我们将优先分析出队,再分析取消;

何时park线程

这个方法调用地方与调用addWaiter一致,即入队后都要判断是否应该park线程。

    /**
     * Checks and updates status for a node that failed to acquire.
     * 一个在获取资源失败的节点检查并更新状态
     * Returns true if thread should block. This is the main signal
     * 返回true当节点应该阻塞时。这时一个主要的信号控制在所有获取循环中;
     * control in all acquire loops.  Requires that pred == node.prev.
     * pred 应该是当前节点的前置节点
     *
     * @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;
        // 前置节点状态为-1表示后继节点需要被唤醒
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * 这个节点已经设置了状态要求释放资源时通知它,
             * to signal it, so it can safely park.
             * 所以它可以安全的park
             */
            return true;
        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
             * 剩下的状态要么是0要么是传播-3.意味着我们需要设置一个信号,
             * need a signal, but don't park yet.  Caller will need to
             * 但尚不park.调用方需要在parkk前尝试确保无法获取资源
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
条件队列入同步队列

我们已经研究完毕获取资源失败时入队,现在来研究直接调用 enq 方法入队的地方;老方法,我们将调用enq的方法列出来:

private Node addWaiter(Node mode);
final boolean transferForSignal(Node node);
final boolean transferAfterCancelledWait(Node node);

第一种场景我们已经调研过了,看剩下的两个方法;

transferForSignal 收到信号而转移

追踪这两个方法的调用处,我们发现只有两处调用这个方法,分别是 doSignal() 和 doSignalAll(),所以理解为收到signal信号后从条件队列转移到同步队列;

    /**
     * Transfers a node from a condition queue onto sync queue.
     * 将一个条件队列的节点转移到同步队列。
     * Returns true if successful.
     * 成功返回true
     * @param node the node
     * @return true if successfully transferred (else the node was
     * cancelled before signal)
     */
    final boolean transferForSignal(Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         * cas 设置状态失败,表明当前节点取消
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * 拼接到同步队列并尝试设置前置节点的waitStaus到-1,
         * indicate that thread is (probably) waiting. If cancelled or
         * 表明当前节点正在等待中。
         * attempt to set waitStatus fails, wake up to resync (in which
         * 如果前置节点取消了或者更改前者节点状态失败了,则唤醒当前节点的线程,
         * case the waitStatus can be transiently and harmlessly wrong).
         * 在这种场景下,当前节点的状态可能是瞬时无害的错误状态。
         */
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }
transferAfterCancelledWait 取消await后转移

await方法是Condition中的方法,我们将在下一篇文章中讲述条件队列的await与唤醒;await取消也仅是指await有时间限制,当释放资源一定时间后,再次抢占资源;时间到期后,称为 cancelledAwait;

    /**
     * Transfers node, if necessary, to sync queue after a cancelled wait.
     * 转移节点,如有需要,将一个取消等待的节点转移到同步节点
     * Returns true if thread was cancelled before being signalled.
     * 返回true,如果已经被取消在通知之前。
     * @param node the node
     * @return true if cancelled before the node was signalled
     */
    final boolean transferAfterCancelledWait(Node node) {
    	
        if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
            enq(node);
            return true;
        }
        /*
         * If we lost out to a signal(), then we can't proceed
         * 如过我们在signal方法竞争失败,我们需要阻塞
         * until it finishes its enq().  Cancelling during an
         * 直到当前节点入队。
         * incomplete transfer is both rare and transient, so just
         * 因为正在取消导致未完成的转移即少见又短暂(cas失败),所以旋转就好
         * spin.
         */
        while (!isOnSyncQueue(node))
            Thread.yield();
        return false;
    }

我们分析了从条件队列节点转移到同步节点的逻辑,但是我们尚未分析条件队列的创建与入队,与出队;这些内容,我们放在第三篇讲解;这里,我们只要理解了进入同步队列的场景就好。

同步队列出队

在acquire 资源入同步队列时,可以看到在获取到资源后的出队操作。分别是setHead与setHeadAndPropagate

获取资源的出队操作
    /**
     * Sets head of queue to be node, thus dequeuing. Called only by
     * 设置头节点未当前节点,实现出队。
     * acquire methods.  Also nulls out unused fields for sake of GC
     * 仅能在获取资源方法中调用。清零不会使用的字段为了GC和压制不必要的信号与遍历
     * and to suppress unnecessary signals and traversals.
     *
     * @param node the node
     */
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

    /**
     * Sets head of queue, and checks if successor may be waiting
     * 设置头节点,并且检查后继接节点可能是一个共享模式下的节点。
     * in shared mode, if so propagating if either propagate > 0 or
     * 如果剩余资源大于0或者已经是传播模式
     * PROPAGATE status was set.
     *
     * @param node the node
     * @param propagate the return value from a tryAcquireShared
     */
    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
         * 		或者被前一个操作记录(在setHead之前或者之后设置h.waitStatus)
         *     or after setHead) by a previous operation
         *     (note: this uses sign-check of waitStatus because
         * 		(注意:这里检查waitStatus因为传播状态可能被设置为通知状态)
         *      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();
        }
    }

阻塞与条件队列的节点被唤醒后,获取到资源后,会再次调用 acquireQueue 将自己从队列中出队;故我们分析完同步队列出队的场景。

取消正在获取的节点

观察上述6种获取的方法,在finally块中都会在发生错误时调用cancelAcquire;我们逐一分析可能出错的常规场景;

public final void acquire(int arg);
// 明显的是捕获到中断异常可能出错
private void doAcquireInterruptibly(int arg)throws InterruptedException;
// 明显的是捕获到中断异常可能出错
private boolean doAcquireNanos(int arg, long nanosTimeout)throws InterruptedException ;
private void doAcquireShared(int arg) ;
// 明显的是捕获到中断异常可能出错
private void doAcquireSharedInterruptibly(int arg)throws InterruptedException;
// 明显的是捕获到中断异常可能出错
private boolean doAcquireSharedNanos(int arg, long nanosTimeout)throws InterruptedException;

剩余的两种场景中,我们无法看出显而易见可能出错的场景,但所有的场景中都会调用 tryAcquire这一个子类实现的方式,很有可能子类实现时基于特殊的业务原因,导致调用报错,为了一个稳定的框架,所以有必要进行节点的取消;

	/**
     * Cancels an ongoing attempt to acquire.
     * 取消一个正在进行获取的意图
     * @param node the node
     */
    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;
        // 只要前置节点的状态为2,都代表节点已经取消了,则找到那个尚未取消的前置节点
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // 前置节点的后继节点是我们要拆分的节点;
        // fail if not, in which case, we lost race vs another cancel
        // cas设置前置节点的后节点失败时,很明显我们失败了竞争,
        // or signal, so no further action is necessary.
        // 这样我们就不用做额外的事情了
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // 可以使用无条件写取代cas写
        // After this atomic step, other Nodes can skip past us.
        // 当这个原子步骤结束后,其他节点就可以跳过我们
        // Before, we are free of interference from other threads.
        // 在这之前,我们不受其他线程影响;这样cas也不会失败,因为我们没有跳过
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        // 如果当前处于队尾,并cas设置队尾为前置节点;进不入该分支,表明我们已经不是队尾
        if (node == tail && compareAndSetTail(node, pred)) {
        	// 进入分支,无论这一步是否成功,都表示我们不再是队尾,且我们是取消的,而且我们失败,意味着别人已经将前置节点的后即节点更改了,do nothing
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // 如果继任者需要唤醒,尝试设置前置节点的下一个节点
            // so it will get one. Otherwise wake it up to propagate.
            // 因此它会得到一个;另一方面我们唤醒下一个节点,使得传播继续
            int ws;
            // 第二个分类,当前节点不是尾节点,不是头节点,且前置节点可以唤醒下一个节点的标志
            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 {
            	// 第三个分类,则是处于头节点,则唤醒下一个节点
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

总结

我们分析了同步队列的创建,获取资源入队,条件对列转移入队,常规出队以及异常取消出队;下一节我们将分析条件队列;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值