AbstractQueuedSynchronizer(AQS)中独占模式与共享模式的设计与实现

1.AQS概览

AbstractQueuedSynchronizer简称AQS,是JUC中实现并发的基础,ReentrantLockCountDownLatchSemaphoreReentrantReadWriteLock底层都是基于AQS实现并发控制的。根据AQS字面含义,其本质上是一个同步队列,主要保存在锁竞争中失败的线程,并在适当的时机唤醒它们,AQS设计成模板方法,获取锁的逻辑则交给子类来实现。大体流程如下:

AQS流程概览.png

本文所讨论的独占模式与共享模式下锁的获取以及释放过程是指:获取锁失败在同步队列中被阻塞,以及持有锁的线程在释放锁后,同步队列中的阻塞线程被唤醒抢夺锁的过程,获取与释放锁的实现都是由子类自己实现,而同步队列只是负责保存因抢夺锁失败而阻塞的线程和被唤醒后成功抢夺锁的线程的出队操作

2.AQS重要变量

我们把一个AQS对象叫做synchronization(同步器),子类会在构造函数中初始化出一个AQS对象。AQS分为两种模式:独占模式与共享模式。

  • exclusiveOwnerThread:The current owner of exclusive mode synchronization(在独占模式中,当一个线程获得锁时,会将该线程设置给该变量保存,会在子类实现获取锁时用到)
  • state:The synchronization state(同步器状态,0代表未获取锁,1代表获取到锁)

进入到AQS中的线程,都会被分配一个Node节点,该节点会记录线程thread,等待状态waitStatus、前驱节点prev以及后继节点next,等待状态会在唤醒节点抢夺锁的逻辑中使用。在AQSNode是载体,可以直观地理解为线程就是Node

static final class Node {
         
        /*标识共享模式*/
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();

        /*标识独占模式*/
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /*线程处于取消状态*/
        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;

        /*后继节点可以被唤醒*/
        /** waitStatus value to indicate successor's thread needs unparking */
        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
         */
        static final int PROPAGATE = -3;

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

        /*前驱节点*/
        volatile Node prev;

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

        /*当前线程*/
        volatile Thread thread;

        /*标识独占模式or共享模式*/
        Node nextWaiter;

    }

3.独占模式下获取锁的过程

我们以ReentrantLock的非公平锁实现为例分析该过程,ReentrantLock调用lock方法的时候,首先会使用AQS提供的CAS操作更新状态,如果更新成功,则将当前线程设置成synchronization的owner;更新失败则调用acquire操作进行入队操作。

final void lock() {

    /*cas操作更新synchronization状态并*/
    if (compareAndSetState(0, 1))
        /*如果更新成功则设置当前线程为synchronization的owner*/
        setExclusiveOwnerThread(Thread.currentThread());
    else
        /*更新失败则调用该方法进行后续的入队操作,重入锁逻辑也在此实现*/
        acquire(1);
}

3.1 调用acquire方法

acquire中主要有三个逻辑

  • 调用子类实现的tryAcquire方法获取锁,获取锁失败则进入下面的操作
  • 调用addWaiter方法对当前获取锁失败的线程进行入队操作
  • 调用acquireQueued方法对当前线程进行阻塞

流程图如下:


acquire流程图.png
public final void acquire(int arg) {

    /*tryAcquire获取锁,该逻辑由子类实现,addWaiter方法将当前线程进行入队操作,acquireQueued将阻塞该线程*/
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

3.2 调用addWaiter方法

addWaiter方法的主要作用是针对当前线程创建节点Node并把节点插入到队尾,分为以下几步:

  • 1.创建新的node存放当前线程,将新创建节点的前驱设置为tail节点
  • 2.cas操作更新尾节点
  • 3.如果cas操作更新成功则将原来的尾部节点的后继节点设置为node,失败则调用enq方法

需要注意的是addWaiter方法处于多线程环境中,只有当step2执行成功,才算成功,整体流程图如下:

addWaiter.png

/**
 * 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) {
    /*创建新的node存放当前线程*/
    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) {
        /*将新创建节点的前驱设置为tail节点*/
        node.prev = pred;
        /*cas操作更新尾节点*/
        if (compareAndSetTail(pred, node)) {
            /*原来的尾部节点的后继节点设置为node*/
            pred.next = node;
            return node;
        }
    }
    /*cas操作失败则进入enq方法*/
    enq(node);
    return node;
}

3.3 调用enq方法进行入队操作

enq方法依然处于并发环境中,也是依靠cas操作来达成目的,有两种情况会进入enq方法,第一种是CLH队列中没有任何节点,第一个进入到该队列中的线程,因为tail==null进入到该方法,在enq中会创建一个空的头节点,并设置尾节点与头节点相同,CLH中的头节点是不存放任何信息的,只是方便用于遍历使用;第二种是入队操作失败的线程,会在该方法中反复重试,直到入队。

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * @param node the node to insert
 * @return node's predecessor
 */
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        /*第一次入队,head和tail还没有被初始化*/
        if (t == null) { // Must initialize
            /*cas操作设置dummyhead,头节点是不放置任何信息的*/
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            /*与addWaiter方法中入队逻辑一致*/
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

3.4 调用acquireQueued方法完成线程的阻塞

调用addWaiter方法后会返回成功入队的节点作为入参传入acquireQueued,进入该方法会判断该节点是否为位于队首的元素即该节点的前一个节点是否为head节点,如果位于队首则尝试获取锁,获取成功则设置新的头节点,失败就进入shouldParkAfterFailedAcquire方法判断是否调用LockSupport.park(this)挂起,被挂起后,当其他节点唤醒该线程的时候,又会重复此过程,唤醒后的操作会在释放锁的章节详细介绍,主要流程图如下:

acquireQueued流程.png

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 * @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);
                /*将前驱节点置空,有助于GC*/
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            /*满足park条件则调用LockSupport.park()方法阻塞该线程,等待唤醒*/
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

接下来重点分析下shouldParkAfterFailedAcquire方法,该方法的作用是设置节点状态,设置的是当前节点的前驱节点的状态,该逻辑主要是在唤醒的时候使用,当前驱节点为头节点且状态是SIGNAL,则会通知唤醒当前节点,此处的实现类比于节点在入队时会告知排在自己前面的节点,自己已经做好要被唤醒的准备,在特定的时刻唤醒自己,如果在之后出现其他状态的变更,比如更新成为CANCELLED状态,则被取消,不需要再被唤醒。

/**
 * 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.
 *
 * @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) {
    /*获取前驱节点的waitStatus*/
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)

        /**
         *前驱节点waitStatus状态设置为SIGNAL后,当前节点就能安全的被park,
         *因为在前驱节点释放锁的时候就能保证会唤醒该节点
         */

        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        /*如果前驱节点是cancelled状态,则跳过,将该节点剔除队列,直到找到非cancel状态的节点*/
        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只会有0这种状态,将前驱节点设置为SIGNAL*/
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire返回true之后,代表该节点下的线程就能被安全的park,因此调用LockSupport.park方法。至此,整个阻塞的过程就算完成了。

    /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

4.独占模式下释放锁的过程

ReentrantLock中非公平锁释放的逻辑为例,本质上是调用AQS提供的release方法

    /**
     * Attempts to release this lock.
     *
     * 

If the current thread is the holder of this lock then the hold * count is decremented. If the hold count is now zero then the lock * is released. If the current thread is not the holder of this * lock then {@link IllegalMonitorStateException} is thrown. * * @throws IllegalMonitorStateException if the current thread does not * hold this lock */ public void unlock() { sync.release(1); }

4.1 release方法调用

release中释放锁的逻辑核心就是unparkSuccessor,最终唤醒线程的逻辑都是放在unparkSuccessor中来实现的。

/**
 * 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}.
 *
 * @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;
        /**
         *确保头节点已经被初始化并且有节点做过入队后已经设置过waitStatus状态
         *因为只有调用过shouldParkAfterFailedAcquire之后waitStatus才会!=0
         *h.waitStatus != 0的逻辑主要是排除头节点已经初始化,但是还没有
         *调用shouldParkAfterFailedAcquire设置waitStatus的情况,这样调
         *用unparkSuccessor时就没有满足条件可以被唤醒的节点,因此不再调用。
         */
        if (h != null && h.waitStatus != 0)
            /*调用unpark方法唤醒后继节点*/
            unparkSuccessor(h);
        return true;
    }
    return false;
}

4.2 unparkSuccessor方法调用

该方法中会以head节点作为入参,首先根据head节点获取到队首的元素(很好实现,只需要调用head节点的next方法),当队首元素为空或者waitStatus大于0(即CANCELLED状态)的时候,会执行在队列中从后往前遍历的操作,找到距离头节点最近且符合唤醒条件(waitStatus == SIGNAL)的节点并调用unpark方法唤醒该节点中的线程。该处有两个比较特殊的情况,第一种是队列中只有一个节点(除头节点外),因为没有后续节点,所以waitStatus值不会被它的后继节点设置,因此等于初始值为0,不满足s == null || s.waitStatus > 0的条件,在最后直接被LockSupport.unpark(s.thread)调用,唤醒;第二种情况,虽然队首没有节点(除头节点外),但还是从队尾往前遍历寻找节点,理想情况是队首为空则代表队列为空,没有遍历队列的必要,但是整个环境是在多线程的情况下,有可能在判空之后,又有新的线程进入到队列之中了。

/**
 * 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.
     */
    int ws = node.waitStatus;
    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;
    /*如果没有节点或者该节点状态为cancel状态,则从队尾往前遍历,找出最前面的符合条件的节点唤醒*/
    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;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

unparkSuccessor代码中还有一个很巧妙的逻辑是遍历队列的时候是从队尾往前遍历,即使是在判空之后,队列中又插入了新的元素,似乎无论是从前往后还是从后到前遍历是没有区别的,显然这块的逻辑是和其他原因有关系的,问题是出在入队的时候,下面的这段代码是addWaiterenq中都有的逻辑,目的是向队尾中插入新的节点。这个操作一共由三步完成,它是在并发环境中并且不是原子操作,所以会出现在cas操作成功之后,t.next = node还没有完成,即部分节点的后继还没有被更新,因此虽然节点已经入队,但是从前往后遍历还是会出现无法找到后继节点的情况。

//1.更新前驱节点
node.prev = t;
//2.cas操作更新尾节点
if (compareAndSetTail(t, node)) {
   //3.更新尾节点成功后,更新后继节点
    t.next = node;
    return t;
}

4.3 线程唤醒后的操作

当在unparkSuccessor中调用LockSupport.unpark(s.thread)后,之前被阻塞的线程就会被唤醒

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

唤醒后首先调用Thread.interrupted()判断该线程是否被中断过(因为在线程被中断的时候也会立马被唤醒),如果被中断过,acquireQueued方法中的interrupted值就为true,因此在acquire方法中,就会调用selfInterrupt方法对线程进行中断,Thread.interrupted()方法的调用会导致中断标记被清除,所以需要调用selfInterrupt再次中断该线程。

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

    static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

这也是为什么ReentrantLocklock方法无法响应中断的原因,即使线程因为中断被唤醒,继续从LockSupport.park(this);处的代码开始执行,但是如果无法获取到锁,依然会被再次阻塞。

5 共享模式下锁的获取

分析共享模式之前,先展示下两种模式下方法的对应关系:

独占模式 共享模式
tryAcquire(int arg) tryAcquireShared(int arg)
acquire(int arg) acquireShared(int arg)
acquireQueued(final Node node, int arg) doAcquireShared(int arg)
tryRelease(int arg) tryReleaseShared(int arg)
release(int arg) releaseShared(int arg)

在共享模式下,锁可以被多个线程同时获取(Semaphore的设计与实现就是基于共享模式下的AQS),因此在获取锁以及释放锁的时候都会唤醒线程去抢夺锁。在Node类中通过nextWaiter来标识共享模式(SHARED)与独占模式(EXCLUSIVE)下的节点。acquireShared方法用于在共享模式下获取锁,其中tryAcquireShared由子类自己实现,当tryAcquireShared失败时,即没有成功获取锁的时候,会调用doAcquireShared方法。

    /**
     * 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);
    }

5.1 调用doAcquireShared获取锁

该方法的实现和acquireQueued非常像,具体可以参照上文的分析对比来看,doAcquireShared创建新节点的时候指定为Node.SHARED共享模式,获取锁之后调用setHeadAndPropagate,该方法主要设置当前节点为头节点并且唤醒同步队列中处于共享模式下的节点。其中setHeadAndPropagate会调用doReleaseShared唤醒同步队列中的线程,释放锁的时候也会调用doReleaseShareddoReleaseShared统一放在后文分析。

/**
 * 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);
                /*r>=0代表获取锁成功,线程被唤醒*/
                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);
    }
}

6 共享模式下锁的释放

6.1 releaseShared释放共享模式下的锁

tryReleaseShared逻辑由子类实现,释放锁成功后调用doReleaseShared唤醒同步队列中的线程。

public final boolean releaseShared(int arg) {
    /*tryReleaseShared释放锁,由子类实现*/
    if (tryReleaseShared(arg)) {
        /*唤醒同步队列中的线程*/
        doReleaseShared();
        return true;
    }
    return false;
}

6.2 doReleaseShared唤醒同步队列中的线程

成功获取加锁和释放锁的时候都会调用该方法唤醒处于同步队列中队首的线程,这里其实是AQS本身的一种优化,加速唤醒同步队列中的元素。对于一次唤醒操作,可以分解为以下几步:

  • 用户代码层面调用子类释放锁代码
  • 释放锁成功之后调用doReleaseShared唤醒同步队列中的头节点,如果头节点无变化该线程退出,否则继续进行for循环
  • 队首节点被唤醒,如果成功获取锁则设置新的头节点(setHeadAndPropagate),调用doReleaseShared方法重复上述逻辑,如果未成功获取到锁则再次被park

unparkSuccessor方法在上文中已经详细分析过了,重点分析下该方法中加速唤醒线程的逻辑,主要靠h == head这段逻辑实现,例举一种doReleaseSharedh == head不成立的场景,用户层面的线程a释放锁之后,位于队首的线程t1被唤醒,t1调用setHeadAndPropagate方法设置头节点为t1,但还未调用doReleaseShared中的unparkSuccessor方法,这时用户层面的线程b释放锁,唤醒位于队首的线程t2,t2调用setHeadAndPropagate设置新的头节点为t2,这个时候t1继续执行,最后发现队首元素已经变化,继续for循环调用unparkSuccessor方法唤醒队首元素。流程如下所示:

doAcquireShared.png

doReleaseSharedh == head不成立时进入for循环持续唤醒同步队列中线程的逻辑,主要是一种加速唤醒的优化逻辑,当头节点发生变化时,说明此时有不止一个线程释放锁,而在共享模式下,锁是能够被不止一个线程所持有的,因此应该趋向于唤醒更多同步队列中的线程来获取锁。

/**
 * 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;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                /*唤醒同步队列队首节点的具体逻辑*/
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        /*如果唤醒其他线程的过程中,头节点未被修改过,则终止*/
        if (h == head)                   // loop if head changed
            break;
    }
}

7 总结

AQS本质上来说就是实现一个队列的功能,因为抢夺锁失败的线程都会被记录到该队列中且被阻塞,在特定的时候出队然后被唤醒,整个流程是遵守FIFO先进先出的规则。共享模式与独占模式的区别主要是在共享模式下(使用共享模式的子类)是支持多个线程获取锁的,围绕此目的,两者的设计会有所不同。

你可能感兴趣的:(AbstractQueuedSynchronizer(AQS)中独占模式与共享模式的设计与实现)