ReentrantLock的條件佇列
在看這篇文章前推薦看下以下兩篇文章
在瞭解Condition的await方法前,推薦先看下signal方法,這個對於後續研究await方法有一定的幫助
除此之外,要了解在Condition中,所有的操作signal、signalAll、await必然都是在已經獲取了鎖的狀態下執行的(例如ReentrantLock),所以很少考慮執行緒安全的問題,這點對分析Condition很有幫助。
1、signal方法
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// firstWaiter指向ConditionQueue的第一個節點,這裡是將原來的節點當成佇列頭
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 原來ConditionQueue的頭節點的nexetWaiter設定為null,方便GC
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
預設的signal方法時喚醒等待佇列中的第一個節點,注意是條件佇列,本文稱為ConditionQueue,這裡要和同步佇列SyncQueue區別開,而且兩者最大的區別在於ConditionQueue頭節點把並不是虛結點,在ConditionQueue中節點狀態都為Condition,所以不需要頭節點儲存signal狀態。
具體來看下transferForSginal()這個方法
/**
* Transfers a node from a condition queue onto sync queue.
* Returns true if successful.
* @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.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* 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;
}
註釋上寫明瞭,transferForSignal這個方法的作用就是將節點從ConditionQueue轉入到SyncQueue,兩個節點的儲存資料結構都是Node,所以可以互相轉化。
預設情況下ConditionQueue中節點的狀態都condition,這裡第一步是將節點Node轉成初始化節點,也就是狀態為0(初始化)。如果轉化失敗了,說明該節點被取消了。這裡不用考慮是否被其他地方同時設定為0,前面說了當前執行sginal方法的必然是拿到鎖的,所以只能有執行緒自己把狀態設定為cancelled。
把狀態設定為0之後,下面呼叫enq方法將節點入隊,並且設定前面的節點為SIGNAL,讓其釋放鎖是可以成功喚醒自己,之後就可以呼叫LockSupport.unpark將當前執行緒喚醒。
signal方法返回true表示成功將ConditionQueue的頭節點放入到SyncQueue中,失敗則返回false。這時我們回到doSginal方法中。
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
可以看到在while迴圈中,如果transferForSignal如果返回false,就會從ConditionQueue中拿下一個節點直到喚醒成功(下一個節點不為null)。
接下來我們來看await方法
2、await方法
await方法的主要作用就是把當前拿到鎖的節點放入到ConditionQueue中,這點和signal方法有點相反的意思。
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await方法中,如果呼叫該方法前,該執行緒就已經被中斷了(Thread.currentThread.interrupt()被呼叫),那麼會立即丟擲InterruptEcxception異常。否則,await方法會將當前節點Node加入到ConditionQueue中。
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
addConditionWaiter方法類似addWaiter,就是將當前執行緒封裝成一個節點,狀態設定為Node.CONDITION,然後放入到ConditionQueue的尾部,不同的是,因為當前執行await的執行緒必然是拿到鎖的,所以它不用考慮到執行緒安全、CAS問題,直接放入到佇列尾部就行,其中ConditionObject(AQS的Condtion實現)用firstWaiter表示ConditionQueue的頭節點,lastWaiter表示ConditionQueue的尾節點。
這裡放入到ConditionQueue之前,會先判斷ConditionQueue最後一個節點的狀態是否為Condition,如果不是就會對ConditionQueue進行整頓。將佇列中的節點狀態為CANCELLED的節點去掉。這裡特意放在await方法中進行處理原因不難想到,當前僅有獲取到鎖的執行緒可以執行await方法,所以放在這裡處理不用考慮複雜的執行緒安全問題。所以這裡unlinkCancelledWaiters進行了簡單的處理。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
unlinkCancelledWaiters的方法不難理解,但是需要提前瞭解以下幾個屬性的意思:
- firstWaiter:表示ConditionQueue中第一個節點
- lastWaiter:表示ConditionQueue中最後一個節點狀態
- t:用來遍歷ConditionQueue中每一個節點
- next:儲存當前節點的下一個節點資訊,如果當前節點的狀態為CANCELLED的話,就能將當前節點去掉並且接上去
- trail:儲存重新遍歷中最新的一個狀態為Condition的節點
首先unlinkCancelledWaiters會先拿出ConditionQueue中的第一個節點資訊,如果狀態不為Condition,那麼就一定是CANCELLED,前面說了當前執行await方法的執行緒是拿到鎖的,所以不會有其他執行緒在一次操作ConditionQueue中的節點,注意之裡說的ConditionQueue的節點,不是SyncQueue的節點,SyncQueue的節點在沒有拿到鎖的時候還是可以修改的(例如在acquireQueue中判斷前一個節點狀態為Cancelled時候,會找前面一個狀態為非Cancelled的節點)。所以這裡如果判斷節點狀態為Cancelled,就需要把節點去掉,並且把firstWaiter指向下一個節點。
如果下一個節點狀態為Condition,那麼該節點就需要保留,並且把firstWaiter長期指向於它。所以這裡就有一個問題,如果再下一個節點狀態為Cancelled的時候,如何保障firstWaiter節點的nextWaiter指向下下個節點,例如如下所示
A(CANCELLED)-> B(CONDITION)->C(CANCELLED)->D(CONDITION)->E(CONDITION)->F(CANCELLED)
如果當前指標firstWaiter指向了B,此時C為CANCELLED,就必須把其去掉,讓D接上B的nextWaiter。這裡trail引用就顯得十分重要了。trail會記錄當前從佇列頭到佇列尾最新的一個節點為CONDITION資訊,當發現下一個節點為CANCELLED(例如C),那麼trail(B)就會將nextWaiter指向D,隨後t引用遍歷到D的時候,因為發現D的狀態為Condition,又會把trail記錄為D。
。。。。剩餘await的方法可以參考前面的兩篇部落格,這裡不再闡述~
相關文章
- 死磕java concurrent包系列(三)基於ReentrantLock理解AQS的條件佇列JavaReentrantLockAQS佇列
- 併發條件佇列之Condition 精講佇列
- 條件佇列大法好:wait和notify的基本語義佇列AI
- AQS原始碼深入分析之條件佇列-你知道Java中的阻塞佇列是如何實現的嗎?AQS原始碼佇列Java
- Laravel 條件陣列 in 的用法Laravel陣列
- 深入理解Java併發框架AQS系列(五):條件佇列(Condition)Java框架AQS佇列
- Java高階:條件佇列與同步器Synchronizer的原理+AQS的應用Java佇列AQS
- Java ReEntrantLock 之 Condition條件(Java程式碼實戰-002)JavaReentrantLock
- Laravel 佇列傳送郵件Laravel佇列
- 每個鎖建立多個條件佇列以避免虛假喚醒佇列
- 死磕java concurrent包系列(四)基於AQS的條件佇列徹底理解ArrayBlockingQueueJavaAQS佇列BloC
- 死磕java concurrent包系列(五)基於AQS的條件佇列把LinkedBlockingQueue“扒光”JavaAQS佇列BloC
- Java併發包原始碼學習系列:詳解Condition條件佇列、signal和awaitJava原始碼佇列AI
- 如何使用ReentrantLock的條件變數,讓多個執行緒順序執行?ReentrantLock變數執行緒
- Laravel 佇列訊息與傳送郵件Laravel佇列
- 陣列模擬佇列 以及佇列的複用(環形佇列)陣列佇列
- Vector用陣列下標訪問的條件陣列
- 佇列、阻塞佇列佇列
- Extjs Grid 清除列頭篩選條件JS
- Linux基礎命令---mailq顯示郵件佇列LinuxAI佇列
- 佇列-單端佇列佇列
- 如何有效的刪除陣列中符合條件的值?陣列
- synchronized 中的同步佇列與等待佇列synchronized佇列
- 阻塞佇列一——java中的阻塞佇列佇列Java
- 使用slice和條件變數實現一個簡單的多生產者多消費者佇列變數佇列
- 【佇列】【懶排序】佇列Q佇列排序
- 佇列 和 迴圈佇列佇列
- laravel中使用利用訊息佇列傳送郵件Laravel佇列
- 佇列的一種實現:迴圈佇列佇列
- Python佇列的三種佇列實現方法Python佇列
- laravel的佇列Laravel佇列
- 【JavaSE】Lock鎖,獨佔鎖ReentrantLock的AQS原始碼,如何管理同步佇列。acquire方法和release方法JavaReentrantLockAQS原始碼佇列UI
- 併發程式設計理論 - AQS之雙向連結串列和條件佇列資料結構程式設計AQS佇列資料結構
- 佇列 手算到機算 入門 佇列 迴圈佇列佇列
- 佇列的順序儲存--迴圈佇列的建立佇列
- 單調佇列雙端佇列佇列
- 圖解--佇列、併發佇列圖解佇列
- 【資料結構】佇列(順序佇列、鏈佇列)的JAVA程式碼實現資料結構佇列Java