併發程式設計理論 - AQS之雙向連結串列和條件佇列資料結構

it_lihongmin發表於2020-11-18

目錄

一、雙向連結串列的Node節點結構

二、AQS結構梳理

三、Condition和ConditionObject結構梳理

四、state與雙向連結串列模型


    根據上一篇理解了Doug Lea設計AQS的意圖和思路,AQS作為抽象的模板方法在其中完成了大量的同步方法的封裝,分為排他和共享兩個模式。所以我們的理解思路就是先看看state值與虛擬雙向連結串列【以及Condition連結串列的關係】,完了再梳理模板方法封裝的同步方法。

一、雙向連結串列的Node節點結構

/**
 * Wait queue node class.
 *
 * <p>The wait queue is a variant of a "CLH" (Craig, Landin, and
 * Hagersten) lock queue. CLH locks are normally used for
 * spinlocks.  We instead use them for blocking synchronizers, but
 * use the same basic tactic of holding some of the control
 * information about a thread in the predecessor of its node.  A
 * "status" field in each node keeps track of whether a thread
 * should block.  A node is signalled when its predecessor
 * releases.  Each node of the queue otherwise serves as a
 * specific-notification-style monitor holding a single waiting
 * thread. The status field does NOT control whether threads are
 * granted locks etc though.  A thread may try to acquire if it is
 * first in the queue. But being first does not guarantee success;
 * it only gives the right to contend.  So the currently released
 * contender thread may need to rewait.
 *
 * <p>To enqueue into a CLH lock, you atomically splice it in as new
 * tail. To dequeue, you just set the head field.
 * <pre>
 *      +------+  prev +-----+       +-----+
 * head |      | <---- |     | <---- |     |  tail
 *      +------+       +-----+       +-----+
 * </pre>
 *
 * <p>Insertion into a CLH queue requires only a single atomic
 * operation on "tail", so there is a simple atomic point of
 * demarcation from unqueued to queued. Similarly, dequeuing
 * involves only updating the "head". However, it takes a bit
 * more work for nodes to determine who their successors are,
 * in part to deal with possible cancellation due to timeouts
 * and interrupts.
 *
 * <p>The "prev" links (not used in original CLH locks), are mainly
 * needed to handle cancellation. If a node is cancelled, its
 * successor is (normally) relinked to a non-cancelled
 * predecessor. For explanation of similar mechanics in the case
 * of spin locks, see the papers by Scott and Scherer at
 * http://www.cs.rochester.edu/u/scott/synchronization/
 *
 * <p>We also use "next" links to implement blocking mechanics.
 * The thread id for each node is kept in its own node, so a
 * predecessor signals the next node to wake up by traversing
 * next link to determine which thread it is.  Determination of
 * successor must avoid races with newly queued nodes to set
 * the "next" fields of their predecessors.  This is solved
 * when necessary by checking backwards from the atomically
 * updated "tail" when a node's successor appears to be null.
 * (Or, said differently, the next-links are an optimization
 * so that we don't usually need a backward scan.)
 *
 * <p>Cancellation introduces some conservatism to the basic
 * algorithms.  Since we must poll for cancellation of other
 * nodes, we can miss noticing whether a cancelled node is
 * ahead or behind us. This is dealt with by always unparking
 * successors upon cancellation, allowing them to stabilize on
 * a new predecessor, unless we can identify an uncancelled
 * predecessor who will carry this responsibility.
 *
 * <p>CLH queues need a dummy header node to get started. But
 * we don't create them on construction, because it would be wasted
 * effort if there is never contention. Instead, the node
 * is constructed and head and tail pointers are set upon first
 * contention.
 *
 * <p>Threads waiting on Conditions use the same nodes, but
 * use an additional link. Conditions only need to link nodes
 * in simple (non-concurrent) linked queues because they are
 * only accessed when exclusively held.  Upon await, a node is
 * inserted into a condition queue.  Upon signal, the node is
 * transferred to the main queue.  A special value of status
 * field is used to mark which queue a node is on.
 *
 * <p>Thanks go to Dave Dice, Mark Moir, Victor Luchangco, Bill
 * Scherer and Michael Scott, along with members of JSR-166
 * expert group, for helpful ideas, discussions, and critiques
 * on the design of this class.
 */
static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final AbstractQueuedSynchronizer.Node SHARED = new AbstractQueuedSynchronizer.Node();
    /** Marker to indicate a node is waiting in exclusive mode */
    static final AbstractQueuedSynchronizer.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;

    /**
     * Status field, taking on only the values:
     *   SIGNAL:      
     *   CANCELLED:  This node is cancelled due to timeout or interrupt.
     *               Nodes never leave this state. In particular,
     *               a thread with cancelled node never again blocks.
     *   CONDITION:  This node is currently on a condition queue.
     *               It will not be used as a sync queue node
     *               until transferred, at which time the status
     *               will be set to 0. (Use of this value here has
     *               nothing to do with the other uses of the
     *               field, but simplifies mechanics.)
     *   PROPAGATE:  A releaseShared should be propagated to other
     *               nodes. This is set (for head node only) in
     *               doReleaseShared to ensure propagation
     *               continues, even if other operations have
     *               since intervened.
     *   0:          None of the above
     *
     * The values are arranged numerically to simplify use.
     * Non-negative values mean that a node doesn't need to
     * signal. So, most code doesn't need to check for particular
     * values, just for sign.
     *
     * The field is initialized to 0 for normal sync nodes, and
     * CONDITION for condition nodes.  It is modified using CAS
     * (or when possible, unconditional volatile writes).
     */
    volatile int waitStatus;

    /**
     * Link to predecessor node that current node/thread relies on
     * for checking waitStatus. Assigned during enqueuing, and nulled
     * out (for sake of GC) only upon dequeuing.  Also, upon
     * cancellation of a predecessor, we short-circuit while
     * finding a non-cancelled one, which will always exist
     * because the head node is never cancelled: A node becomes
     * head only as a result of successful acquire. A
     * cancelled thread never succeeds in acquiring, and a thread only
     * cancels itself, not any other node.
     */
    volatile AbstractQueuedSynchronizer.Node prev;

    /**
     * Link to the successor node that the current node/thread
     * unparks upon release. Assigned during enqueuing, adjusted
     * when bypassing cancelled predecessors, and nulled out (for
     * sake of GC) when dequeued.  The enq operation does not
     * assign next field of a predecessor until after attachment,
     * so seeing a null next field does not necessarily mean that
     * node is at end of queue. However, if a next field appears
     * to be null, we can scan prev's from the tail to
     * double-check.  The next field of cancelled nodes is set to
     * point to the node itself instead of null, to make life
     * easier for isOnSyncQueue.
     */
    volatile AbstractQueuedSynchronizer.Node next;

    /**
     * The thread that enqueued this node.  Initialized on
     * construction and nulled out after use.
     */
    volatile Thread thread;

    /**
     * Link to next node waiting on condition, or the special
     * value SHARED.  Because condition queues are accessed only
     * when holding in exclusive mode, we just need a simple
     * linked queue to hold nodes while they are waiting on
     * conditions. They are then transferred to the queue to
     * re-acquire. And because conditions can only be exclusive,
     * we save a field by using special value to indicate shared
     * mode.
     */
    AbstractQueuedSynchronizer.Node nextWaiter;

    /**
     * Returns true if node is waiting in shared mode.
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * Returns previous node, or throws NullPointerException if null.
     * Use when predecessor cannot be null.  The null check could
     * be elided, but is present to help the VM.
     *
     * @return the predecessor of this node
     */
    final AbstractQueuedSynchronizer.Node predecessor() throws NullPointerException {
        AbstractQueuedSynchronizer.Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, AbstractQueuedSynchronizer.Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

還是看看Daug Lea對Node的註釋說明:

<p> 等待佇列是“CLH”(Craig Landin Hagersten)變體是雙向鎖佇列,通常用於自旋鎖。使用雙向佇列來實現阻塞的同步所有的任務,但是使用基本的策略來控制當前節點的前置節點的執行緒資訊,使用volatile state欄位就控制了雙向佇列中的所有節點的同步阻塞,當前一個節點釋放時,後一個節點會被標記為SIGNAL,佇列中的每個節點作為一個【特殊通知型別】會持有管程中的等待執行緒,volatile state欄位不控制執行緒授權的鎖。一個執行緒可以呼叫acquire方法(排他或共享)去設定為佇列頭部,但是可能不成功,則需要重新等待。

<p> 想要進入CLH鎖(呼叫enq方法),則預設會以一個新的節點加入隊尾,要退出佇列,只需將節點(當前Node)的Head欄位值為空【連結串列找不到】。

<pre>
       +------+  prev +-----+       +-----+
  head |      | <---- |     | <---- |     |  tail
       +------+       +-----+       +-----+
</pre>

<p> 想要插入佇列則需要使用CAS原子操作插入雙向佇列的尾部,同樣的如果想退出佇列則需要將前置節點欄位置位null。只是該操作需要耗時將後續的鏈組裝成串,如果某些節點因為超時而取消、執行中斷(interrupt)則也需要調整。

<p> 雙向連結串列的前置連結在正常的CLH鎖中不使用,主要是用於節點的取消操作,當節點取消後需要後繼節點重新連結到一個未取消的節點。

<p> 一般情況next鏈實現阻塞機制,每個節點的執行緒id儲存到自己的Node中(volatile Thread thread屬性)。所以前一個節點執行完成後,會喚醒下一個節點也就確定了它是哪個執行緒。成功喚醒下一個節點後就需要避免與剛加入佇列的節點的競爭(即新加入的節點一定在被成功喚醒的節點後面),所以必要時需要原子操作(CAS)更新到最後,防止某一個節點的next為空(則雙向連結串列就斷了)。(換句話說,下一個連結是一個優化這樣我們就不需要逆向掃描了)

<p> 取消操作使用比較保守的演算法(犧牲效能,一定保證雙向連結串列完整)。

<p> CLH佇列需要一個虛擬的節點頭才能往下走,但是我們在構造器中並沒有初始化Head節點(防止不使用的話浪費資源,懶載入或者叫懶初始化),所以頭結點是在使用構造初始化第一個節點時設定。如下:

Node() {    // Used to establish initial head or SHARED marker
}

Node(Thread thread, AbstractQueuedSynchronizer.Node mode) {     // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
}

<p> 管程模型的條件佇列在這裡可以允許有多個,但是等待的執行緒使用節點與其他正常的節點一樣,只是需要單獨增加一個連結串列。而這些連結串列(多個ConditionObject物件,正常可以通過ReentrantLock的newCondition方法建立)只需要連結到一個普通的Node節點即可,因為當前的節點一定是獨佔模式(即Node的EXCLUSIVE屬性不為空)。呼叫Condition的await時,節點插入新增佇列中,呼叫signal時才真正地轉到主佇列(CLH)佇列中,並且使用專門的欄位nextWaiter來標記節點在哪個佇列上。

 

waitStatus

每個節點都有自己的狀態:volatile int waitStatus屬性

/** 任務取消時的節點狀態 */
static final int CANCELLED =  1;

/** 需要喚醒執行緒(當前節點的 volatile Thread thread屬性)的狀態 */
static final int SIGNAL    = -1;

/** 需要等待條件佇列滿足的狀態 */
static final int CONDITION = -2;

/** 下一個共享節點的應該無條件地傳播 */
static final int PROPAGATE = -3;

 

二、AQS結構梳理

    AQS唯一繼承了AbstractOwnableSynchronizer父類,而其中只有一個屬性用於儲存排他模式的【當前】執行緒(並提供setter、getter方法),那麼其他執行緒進入的時候只需要判斷其執行緒是否為空 或者 當前執行緒是否與排他執行緒相同就可以排他了。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements Serializable {
}

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {
    private static final long serialVersionUID = 3737899427754241961L;

    protected AbstractOwnableSynchronizer() { }

    /** The current owner of exclusive mode synchronization. */
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}

設計了兩個內部類Node(雙向連結串列的節點)、ConditionObject(管程模型的條件佇列、單向連結串列),兩個內部類後面分析,其資料結構【屬性】還是比較簡單的,那麼重要的就是內部類和操作方法(佇列的同步操作、條件佇列的插隊)

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements Serializable {

    /** volatle state【解決可見性和有序性】 + unsafe【CAS解決原子性】 */
    private volatile int state;
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    /** 虛擬雙向連結串列的頭和尾節點 */
    private transient volatile Node head;
    private transient volatile Node tail;

    // 其他同步方法,以及虛擬雙向量的操作方法,省略
}

三、Condition和ConditionObject結構梳理

/**
 * {@code Condition} factors out the {@code Object} monitor
 * methods ({@link Object#wait() wait}, {@link Object#notify notify}
 * and {@link Object#notifyAll notifyAll}) into distinct objects to
 * give the effect of having multiple wait-sets per object, by
 * combining them with the use of arbitrary {@link Lock} implementations.
 * Where a {@code Lock} replaces the use of {@code synchronized} methods
 * and statements, a {@code Condition} replaces the use of the Object
 * monitor methods.
 *
 * <p>Conditions (also known as <em>condition queues</em> or
 * <em>condition variables</em>) provide a means for one thread to
 * suspend execution (to &quot;wait&quot;) until notified by another
 * thread that some state condition may now be true.  Because access
 * to this shared state information occurs in different threads, it
 * must be protected, so a lock of some form is associated with the
 * condition. The key property that waiting for a condition provides
 * is that it <em>atomically</em> releases the associated lock and
 * suspends the current thread, just like {@code Object.wait}.
 *
 * <p>A {@code Condition} instance is intrinsically bound to a lock.
 * To obtain a {@code Condition} instance for a particular {@link Lock}
 * instance use its {@link Lock#newCondition newCondition()} method.
 *
 * <p>As an example, suppose we have a bounded buffer which supports
 * {@code put} and {@code take} methods.  If a
 * {@code take} is attempted on an empty buffer, then the thread will block
 * until an item becomes available; if a {@code put} is attempted on a
 * full buffer, then the thread will block until a space becomes available.
 * We would like to keep waiting {@code put} threads and {@code take}
 * threads in separate wait-sets so that we can use the optimization of
 * only notifying a single thread at a time when items or spaces become
 * available in the buffer. This can be achieved using two
 * {@link Condition} instances.
 * <pre>
 * class BoundedBuffer {
 *   <b>final Lock lock = new ReentrantLock();</b>
 *   final Condition notFull  = <b>lock.newCondition(); </b>
 *   final Condition notEmpty = <b>lock.newCondition(); </b>
 *
 *   final Object[] items = new Object[100];
 *   int putptr, takeptr, count;
 *
 *   public void put(Object x) throws InterruptedException {
 *     <b>lock.lock();
 *     try {</b>
 *       while (count == items.length)
 *         <b>notFull.await();</b>
 *       items[putptr] = x;
 *       if (++putptr == items.length) putptr = 0;
 *       ++count;
 *       <b>notEmpty.signal();</b>
 *     <b>} finally {
 *       lock.unlock();
 *     }</b>
 *   }
 *
 *   public Object take() throws InterruptedException {
 *     <b>lock.lock();
 *     try {</b>
 *       while (count == 0)
 *         <b>notEmpty.await();</b>
 *       Object x = items[takeptr];
 *       if (++takeptr == items.length) takeptr = 0;
 *       --count;
 *       <b>notFull.signal();</b>
 *       return x;
 *     <b>} finally {
 *       lock.unlock();
 *     }</b>
 *   }
 * }
 * </pre>
 *
 * (The {@link java.util.concurrent.ArrayBlockingQueue} class provides
 * this functionality, so there is no reason to implement this
 * sample usage class.)
 *
 * <p>A {@code Condition} implementation can provide behavior and semantics
 * that is
 * different from that of the {@code Object} monitor methods, such as
 * guaranteed ordering for notifications, or not requiring a lock to be held
 * when performing notifications.
 * If an implementation provides such specialized semantics then the
 * implementation must document those semantics.
 *
 * <p>Note that {@code Condition} instances are just normal objects and can
 * themselves be used as the target in a {@code synchronized} statement,
 * and can have their own monitor {@link Object#wait wait} and
 * {@link Object#notify notification} methods invoked.
 * Acquiring the monitor lock of a {@code Condition} instance, or using its
 * monitor methods, has no specified relationship with acquiring the
 * {@link Lock} associated with that {@code Condition} or the use of its
 * {@linkplain #await waiting} and {@linkplain #signal signalling} methods.
 * It is recommended that to avoid confusion you never use {@code Condition}
 * instances in this way, except perhaps within their own implementation.
 *
 * <p>Except where noted, passing a {@code null} value for any parameter
 * will result in a {@link NullPointerException} being thrown.
 *
 * <h3>Implementation Considerations</h3>
 *
 * <p>When waiting upon a {@code Condition}, a &quot;<em>spurious
 * wakeup</em>&quot; is permitted to occur, in
 * general, as a concession to the underlying platform semantics.
 * This has little practical impact on most application programs as a
 * {@code Condition} should always be waited upon in a loop, testing
 * the state predicate that is being waited for.  An implementation is
 * free to remove the possibility of spurious wakeups but it is
 * recommended that applications programmers always assume that they can
 * occur and so always wait in a loop.
 *
 * <p>The three forms of condition waiting
 * (interruptible, non-interruptible, and timed) may differ in their ease of
 * implementation on some platforms and in their performance characteristics.
 * In particular, it may be difficult to provide these features and maintain
 * specific semantics such as ordering guarantees.
 * Further, the ability to interrupt the actual suspension of the thread may
 * not always be feasible to implement on all platforms.
 *
 * <p>Consequently, an implementation is not required to define exactly the
 * same guarantees or semantics for all three forms of waiting, nor is it
 * required to support interruption of the actual suspension of the thread.
 *
 * <p>An implementation is required to
 * clearly document the semantics and guarantees provided by each of the
 * waiting methods, and when an implementation does support interruption of
 * thread suspension then it must obey the interruption semantics as defined
 * in this interface.
 *
 * <p>As interruption generally implies cancellation, and checks for
 * interruption are often infrequent, an implementation can favor responding
 * to an interrupt over normal method return. This is true even if it can be
 * shown that the interrupt occurred after another action that may have
 * unblocked the thread. An implementation should document this behavior.
 *
 * @since 1.5
 * @author Doug Lea
 */
public interface Condition {

    void await() throws InterruptedException;

    void awaitUninterruptibly();

    /**
     *  使用的正確姿勢:
     *  <pre> {@code
     * boolean aMethod(long timeout, TimeUnit unit) {
     *   long nanos = unit.toNanos(timeout);
     *   lock.lock();
     *   try {
     *     while (!conditionBeingWaitedFor()) {
     *       if (nanos <= 0L)
     *         return false;
     *       nanos = theCondition.awaitNanos(nanos);
     *     }
     *     // ...
     *   } finally {
     *     lock.unlock();
     *   }
     * }}</pre>
     */
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    boolean await(long time, TimeUnit unit) throws InterruptedException;

    /** 使用的正確姿勢:
     *  <pre> {@code
     * boolean aMethod(Date deadline) {
     *   boolean stillWaiting = true;
     *   lock.lock();
     *   try {
     *     while (!conditionBeingWaitedFor()) {
     *       if (!stillWaiting)
     *         return false;
     *       stillWaiting = theCondition.awaitUntil(deadline);
     *     }
     *     // ...
     *   } finally {
     *     lock.unlock();
     *   }
     * }}</pre>
     */
    boolean awaitUntil(Date deadline) throws InterruptedException;
  
    void signal();

    void signalAll();
}

    Condition相當於synchronized管程模型中,Object的waitnotifynotifyAll。預製對應的就是Condition定義的Condition的 await*signalsignalAll方法。只是該管程模型中執行多個條件佇列【synchronized(Object)只能放入一個物件,則只能執行一個條件佇列】,而每一個條件佇列(連結串列)與juc的Lock相對應。之前分析synchronized管程模型的正確使用姿勢(併發程式設計基礎 - synchronized使用場景和等待喚醒機制的正確姿勢),即Object的等待喚醒方法如果不在鎖中使用則會丟擲異常,當然這裡也規定了Condition管程模型的正確使用姿勢(如上註釋,並且也規定了awaitNanosawaitUtil的正確使用姿勢):

<pre>
class BoundedBuffer {
  <b>final Lock lock = new ReentrantLock();</b>
  final Condition notFull  = <b>lock.newCondition(); </b>
  final Condition notEmpty = <b>lock.newCondition(); </b>
 
  final Object[] items = new Object[100];
  int putptr, takeptr, count;
 
  public void put(Object x) throws InterruptedException {
    <b>lock.lock();
    try {</b>
      while (count == items.length)
        <b>notFull.await();</b>
      items[putptr] = x;
      if (++putptr == items.length) putptr = 0;
      ++count;
      <b>notEmpty.signal();</b>
    <b>} finally {
      lock.unlock();
    }</b>
  }
 
  public Object take() throws InterruptedException {
    <b>lock.lock();
    try {</b>
      while (count == 0)
        <b>notEmpty.await();</b>
      Object x = items[takeptr];
      if (++takeptr == items.length) takeptr = 0;
      --count;
      <b>notFull.signal();</b>
      return x;
    <b>} finally {
      lock.unlock();
    }</b>
  }
}
</pre>

 

四、state與雙向連結串列模型

梳理完上面的註釋和AQS結構之後可以理解,在沒有Condition物件時的模型,以及有多個Condition時(當前節點為獨享)模型對比如下:

      

 

 

 

 

 

 

 

 

相關文章