併發程式設計理論 - AQS之雙向連結串列和條件佇列資料結構
目錄
三、Condition和ConditionObject結構梳理
根據上一篇理解了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 "wait") 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 "<em>spurious
* wakeup</em>" 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的wait、notify、notifyAll。預製對應的就是Condition定義的Condition的 await*、signal、signalAll方法。只是該管程模型中執行多個條件佇列【synchronized(Object)只能放入一個物件,則只能執行一個條件佇列】,而每一個條件佇列(連結串列)與juc的Lock相對應。之前分析synchronized管程模型的正確使用姿勢(併發程式設計基礎 - synchronized使用場景和等待喚醒機制的正確姿勢),即Object的等待喚醒方法如果不在鎖中使用則會丟擲異常,當然這裡也規定了Condition管程模型的正確使用姿勢(如上註釋,並且也規定了awaitNanos、awaitUtil的正確使用姿勢):
<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時(當前節點為獨享)模型對比如下:
相關文章
- 資料結構--陣列、單向連結串列、雙向連結串列資料結構陣列
- 資料結構之雙向連結串列資料結構
- 資料結構實驗之連結串列九:雙向連結串列資料結構
- 資料結構——雙向連結串列資料結構
- 資料結構:雙向連結串列資料結構
- 畫江湖之資料結構【第一話:連結串列】雙向連結串列資料結構
- 畫江湖之資料結構 [第一話:連結串列] 雙向連結串列資料結構
- 資料結構之陣列和連結串列資料結構陣列
- 佇列_單向連結串列佇列
- Linux 核心資料結構:雙向連結串列Linux資料結構
- 資料結構(雙向連結串列的實現)資料結構
- python 資料結構之雙向連結串列的實現Python資料結構
- Python內建資料結構之雙向佇列Python資料結構佇列
- JavaScript資料結構之連結串列--設計JavaScript資料結構
- 結構與演算法(03):單向連結串列和雙向連結串列演算法
- 資料結構之「雙端佇列」資料結構佇列
- javascript中的連結串列結構—雙向連結串列JavaScript
- 連結串列-雙向連結串列
- 資料結構-雙向連結串列(Python實現)資料結構Python
- JS資料結構第三篇---雙向連結串列和迴圈連結串列之約瑟夫問題JS資料結構
- 資料結構之連結串列與陣列(1):陣列和連結串列的簡介資料結構陣列
- 資料結構之連結串列與陣列(3):單向連結串列上的簡單操作資料結構陣列
- 資料結構學習(C++)——雙向連結串列 (轉)資料結構C++
- 畫江湖之資料結構【第一話:連結串列】單向連結串列資料結構
- 畫江湖之資料結構 [第一話:連結串列] 單向連結串列資料結構
- 資料結構實驗之連結串列四:有序連結串列的歸併資料結構
- 資料結構與演算法——連結串列 Linked List(單連結串列、雙向連結串列、單向環形連結串列-Josephu 問題)資料結構演算法
- 雙向連結串列
- JUC併發程式設計基石AQS原始碼之結構篇程式設計AQS原始碼
- 資料結構與演算法(三) -- 線性表之雙向連結串列資料結構演算法
- Java學習筆記:資料結構之線性表(雙向連結串列)Java筆記資料結構
- 資料結構之php實現單向連結串列資料結構PHP
- 深入淺出AQS之條件佇列AQS佇列
- 資料結構之「連結串列」資料結構
- 資料結構之連結串列資料結構
- 資料結構與演算法(二)佇列、棧、連結串列資料結構演算法佇列
- 資料結構之連結串列與陣列(2):單向連結串列上的簡單操作問題資料結構陣列
- 資料結構-棧(通過陣列和單向連結串列實現)資料結構陣列