并发编程理论 - AQS之双向链表和条件队列数据结构

目录

一、双向链表的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队列时的模型对比:

      

 

 

 

 

 

 

 

 

 

相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页