系列傳送門:
- Java併發包原始碼學習系列:AbstractQueuedSynchronizer
- Java併發包原始碼學習系列:CLH同步佇列及同步資源獲取與釋放
- Java併發包原始碼學習系列:AQS共享式與獨佔式獲取與釋放資源的區別
- Java併發包原始碼學習系列:ReentrantLock可重入獨佔鎖詳解
- Java併發包原始碼學習系列:ReentrantReadWriteLock讀寫鎖解析
Condition介面
Contition是一種廣義上的條件佇列,它利用await()和signal()為執行緒提供了一種更為靈活的等待/通知模式。
圖源:《Java併發程式設計的藝術》
Condition必須要配合Lock一起使用,因為對共享狀態變數的訪問發生在多執行緒環境下。
一個Condition的例項必須與一個Lock繫結,因此await和signal的呼叫必須在lock和unlock之間,有鎖之後,才能使用condition嘛。以ReentrantLock為例,簡單使用如下:
public class ConditionTest {
public static void main(String[] args) {
final ReentrantLock lock = new ReentrantLock();
final Condition condition = lock.newCondition();
Thread thread1 = new Thread(() -> {
String name = Thread.currentThread().getName();
lock.lock();
System.out.println(name + " <==成功獲取到鎖" + lock);
try {
System.out.println(name + " <==進入條件佇列等待");
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " <==醒了");
lock.unlock();
System.out.println(name + " <==釋放鎖");
}, "等待執行緒");
thread1.start();
Thread thread2 = new Thread(() -> {
String name = Thread.currentThread().getName();
lock.lock();
System.out.println(name + " ==>成功獲取到鎖" + lock);
try {
System.out.println("========== 這裡演示await中的執行緒沒有被signal的時候會一直等著 ===========");
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " ==>通知等待佇列的執行緒");
condition.signal();
lock.unlock();
System.out.println(name + " ==>釋放鎖");
}, "通知執行緒");
thread2.start();
}
}
等待執行緒 <==成功獲取到鎖java.util.concurrent.locks.ReentrantLock@3642cea8[Locked by thread 等待執行緒]
等待執行緒 <==進入條件佇列等待
通知執行緒 ==>成功獲取到鎖java.util.concurrent.locks.ReentrantLock@3642cea8[Locked by thread 通知執行緒]
========== 這裡演示await中的執行緒沒有被signal的時候會一直等著 ===========
通知執行緒 ==>通知等待佇列的執行緒
通知執行緒 ==>釋放鎖
等待執行緒 <==醒了
等待執行緒 <==釋放鎖
接下來我們將從原始碼的角度分析上面這個流程,理解所謂條件佇列的內涵。
AQS條件變數的支援之ConditionObject內部類
AQS,Lock,Condition,ConditionObject
之間的關係:
ConditionObject是AQS的內部類,實現了Condition介面,Lock中提供newCondition()方法,委託給內部AQS的實現Sync來建立ConditionObject物件,享受AQS對Condition的支援。
// ReentrantLock#newCondition
public Condition newCondition() {
return sync.newCondition();
}
// Sync#newCondition
final ConditionObject newCondition() {
// 返回Contition的實現,定義在AQS中
return new ConditionObject();
}
ConditionObject用來結合鎖實現執行緒同步,ConditionObject可以直接訪問AQS物件內部的變數,比如state狀態值和AQS佇列。
ConditionObject是條件變數,每個條件變數對應一個條件佇列(單向連結串列佇列),其用來存放呼叫條件變數的await方法後被阻塞的執行緒,ConditionObject維護了首尾節點,沒錯這裡的Node就是我們之前在學習AQS的時候見到的那個Node,我們會在下面回顧:
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** 條件佇列的第一個節點. */
private transient Node firstWaiter;
/** 條件佇列的最後一個節點. */
private transient Node lastWaiter;
}
看到這裡我們需要明確這裡的條件佇列和我們之前說的AQS同步佇列是不一樣的:
- AQS維護的是當前在等待資源的佇列,Condition維護的是在等待signal訊號的佇列。
- 每個執行緒會存在上述兩個佇列中的一個,lock與unlock對應在AQS佇列,signal與await對應條件佇列,執行緒節點在他們之間反覆橫跳。
這裡著重說明一下,接下來的原始碼學習部分,我們會將兩個佇列進行區分,涉及到同步佇列和阻塞佇列的描述,意味著是AQS的同步佇列,而條件佇列指的是Condition佇列,望讀者知曉。
這裡我們針對上面的demo來分析一下會更好理解一些:
為了簡化,接下來我將用D表示等待執行緒,用T表示通知執行緒。
- 【D】先呼叫
lock.lock()
方法,此時無競爭,【D】被加入到AQS同步佇列中。 - 【D】呼叫
condition.await()
方法,此時【D】被構建為等待節點並加入到condition對應的條件等待佇列中,並從AQS同步佇列中移除。 - 【D】陷入等待之後,【T】啟動,由於AQS佇列中的【D】已經被移除,此時【T】也很快獲取到鎖,相應的,【T】也被加入到AQS同步佇列中。
- 【T】接著呼叫
condition.signal()
方法,這時condition對應的條件佇列中只有一個節點【D】,於是【D】被取出,並被再次加入AQS的等待佇列中。此時【D】並沒有被喚醒,只是單純換了個位置。 - 接著【T】執行
lock.unlock()
,釋放鎖鎖之後,會喚醒AQS佇列中的【D】,此時【D】真正被喚醒且執行。
OK,lock -> await -> signal -> unlock
這一套流程相信已經大概能夠理解,接下來我們試著看看原始碼吧。
回顧AQS中的Node
我們這裡再簡單回顧一下AQS中Node類與Condition相關的欄位:
// 記錄當前執行緒的等待狀態,
volatile int waitStatus;
// 前驅節點
volatile Node prev;
// 後繼節點
volatile Node next;
// node儲存的執行緒
volatile Thread thread;
// 當前節點在Condition中等待佇列上的下一個節點
Node nextWaiter;
waitStatus可以取五種狀態:
- 初始化為0,啥也不表示,之後會被置signal。
- 1表示cancelled,取消當前執行緒對鎖的爭奪。
- -1表示signal,表示當前節點釋放鎖後需要喚醒後面可被喚醒的節點。
- -2表示condition,我們這篇的重點,表示當前節點在條件佇列中。
- -3表示propagate,表示釋放共享資源的時候會向後傳播釋放其他共享節點。
當然,除了-2這個condition狀態,其他的等待狀態我們之前都或多或少分析過,今天著重學習condition這個狀態的意義。
我們還可以看到一個Node型別的nextWaiter,它表示條件佇列中當前節點的下一個節點,可以看出用以實現條件佇列的單向連結串列。
void await()
呼叫Condition的await()方法,會使當前執行緒進入等待佇列並釋放鎖,同時執行緒狀態變為等待狀態。
其實就是從AQS同步佇列的首節點,注意不是head,而是獲取了鎖的節點,移動到Condition的等待佇列中。
瞭解這些之後,我們直接來看看具體方法的原始碼:
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;
}
// 上面的迴圈退出有兩種情況:
// 1. isOnSyncQueue(node) 為true,即當前的node已經轉移到阻塞佇列了
// 2. checkInterruptWhileWaiting != 0, 表示執行緒中斷
// 退出迴圈,被喚醒之後,進入阻塞佇列,等待獲取鎖 acquireQueued
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
新增到條件佇列
Node addConditionWaiter()
addConditionWaiter() 是將當前節點加入到條件佇列中:
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果lastWaiter被取消了,將其清除
if (t != null && t.waitStatus != Node.CONDITION) {
// 遍歷整個條件佇列,將已取消的所有節點清除出列
unlinkCancelledWaiters();
// t重新賦值一下,因為last可能改變了
t = lastWaiter;
}
//注意這裡,node在初始化的時候,會指定ws為CONDITION
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// t == null 表示佇列此時為空,初始化firstWaiter
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;// 入隊尾
lastWaiter = node;// 將尾指標指向新建的node
return node;
}
void unlinkCancelledWaiters()
unlinkCancelledWaiters用於清除佇列中已經取消等待的節點。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// trail這裡表示取消節點的前驅節點
Node trail = null;
// t會從頭到尾遍歷這個單連結串列
while (t != null) {
// next用於儲存下一個
Node next = t.nextWaiter;
// 如果發現當前這個節點 不是 condition了, 那麼考慮移除它
// 下面是單連結串列的移除節點操作 簡單來說就是 trail.next = t.next
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
// 說明first就是不是condition了
if (trail == null)
firstWaiter = next;
else
//trail.next = t.next
trail.nextWaiter = next;
// trail後面沒東西,自然trail就是lastWaiter了
if (next == null)
lastWaiter = trail;
}
// 當前節點是一直跟到不是condition節點的上一個
else
trail = t;
// 向後遍歷 t = t.next
t = next;
}
}
總結一下addConditionWaiter的過程:
- 首先判斷條件佇列的尾節點是否被取消了,這裡用last.ws != CONDITION來判斷,如果是的話,就需要從頭到尾遍歷,消除被不是condition的節點。
- 接著將當前執行緒包裝為Node,指定ws為CONDITION。
完全釋放獨佔鎖
將節點加入等待佇列中後,就需要完全釋放執行緒擁有的獨佔鎖了,完全釋放針對重入鎖的情況。我們可以拉到await()方法中看看,將會呼叫:int savedState = fullyRelease(node);
,這句話有什麼內涵呢?
我們看到這個方法返回了一個savedState變數,簡單的理解就是儲存狀態。我們知道重入鎖的state由重入的次數,如果一個state為N,我們可以認為它持有N把鎖。
await()方法必須將state置0,也就是完全釋放鎖,後面的執行緒才能獲取到這把鎖,置0之後,我們需要用個變數標記一下,也就是這裡的savedState。
這樣它被重新喚醒的時候,我們就知道,他需要獲取savedState把鎖。
int fullyRelease(Node node)
final int fullyRelease(Node node) {
boolean failed = true;
try {
// 獲取當前的state值,重入次數
int savedState = getState();
// 釋放N = savedState資源
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
// 如果獲取失敗,將會將節點設定為取消狀態,並丟擲異常
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
這裡其實我們就會明白開頭說的:如果某個執行緒沒有獲取lock,就直接呼叫condition的await()方法,結果是什麼呢,在release的時候丟擲異常,然後節點被取消,之後節點進來的時候,將它清理掉。
等待進入阻塞佇列
ok,完全釋放鎖之後,將會來到這幾步,如果這個節點的執行緒不在同步佇列中,說明該執行緒還不具備競爭鎖的資格,將被一直掛起,這裡的同步佇列指的是AQS的阻塞佇列。
int interruptMode = 0;
// 如果這個節點的執行緒不在同步佇列中,說明該執行緒還不具備競爭鎖的資格,會一直掛起
while (!isOnSyncQueue(node)) {
// 掛起執行緒
LockSupport.park(this);
// 如果執行緒中斷,退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
boolean isOnSyncQueue(Node node)
下面這個方法會判斷節點是不是已經到阻塞佇列中了,如果是的話,就直接返回true,這個方法的必要性是什麼呢?
其實啊,這裡需要提前說一下signal()方法,signal的作用和await()方法,將在等待佇列中阻塞的節點移動到AQS同步佇列中,這個方法就是說判斷一下這個節點是不是移過去了。
final boolean isOnSyncQueue(Node node) {
// 1. 節點的等待狀態還是condition表示還在等待佇列中
// 2. node.prev == null 表示還沒移到阻塞佇列中[prev和next都是阻塞佇列中用的]
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果node已經有了後繼節點,表示已經在阻塞佇列中了
if (node.next != null) // If has successor, it must be on queue
return true;
/*
* node.prev can be non-null, but not yet on queue because
* the CAS to place it on queue can fail. So we have to
* traverse from tail to make sure it actually made it. It
* will always be near the tail in calls to this method, and
* unless the CAS failed (which is unlikely), it will be
* there, so we hardly ever traverse much.
*/
// 來到這裡的情況:ws != condition && node.prev != null && node.next == null
// 想想:為什麼node.prev != null不能作為判斷不在阻塞佇列的依據呢?
// CAS首先設定node.prev 指向tail,這個時候node.prev 是不為null的,但CAS可能會失敗
return findNodeFromTail(node);
}
為什麼node.prev != null不能作為判斷不在阻塞佇列的依據呢?
官方給出瞭解答: 因為CAS的入隊操作中,首先設定node.prev 指向tail,這個時候node.prev 是不為null的。你能夠說他入隊成功一定成功嗎?不一定,因為CAS可能會失敗,所以要findNodeFromTail(node)。
boolean findNodeFromTail(Node node)
從阻塞佇列的尾部向前遍歷,如果找到這個node,表示它已經在了,那就返回true。
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
// 已經有了
if (t == node)
return true;
// 尾都沒有,找啥呢,返回false
if (t == null)
return false;
// 一直往前找
t = t.prev;
}
}
void signal()
由於之前節點被加入等待佇列將會一直阻塞,為了連貫性,我們來看看喚醒它的signal方法吧:
之前說到,如果這個執行緒會在等待佇列中等待,那麼喚醒它的signal方法的流程是怎麼樣的呢,前面其實已經說了一丟丟了,我們猜測,signal會將isOnSyncQueue方法的迴圈打破,接下來看看是不是這樣子的。
public final void signal() {
// 一樣的,必須佔有當前這個鎖才能用signal方法
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
喚醒節點
該方法會從頭到尾遍歷條件佇列,找到需要移到同步佇列的節點。
void doSignal(Node first)
private void doSignal(Node first) {
do {
// firstWaiter 指向first的下一個
if ( (firstWaiter = first.nextWaiter) == null)
// 如果first是最後一個且要被移除了,就將last置null
lastWaiter = null;
// first斷絕與條件佇列的連線
first.nextWaiter = null;
// fisrt轉移失敗,就看看後面是不是需要的
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
這裡的while迴圈表示,如果first沒有轉移成功,就接著判斷first後面的節點是不是需要轉移。
boolean transferForSignal(Node node)
該方法將節點從條件佇列轉移到阻塞佇列。
final boolean transferForSignal(Node node) {
/*
* CAS操作嘗試將Condition的節點的ws改為0
* 如果失敗,意味著:節點的ws已經不是CONDITION,說明節點已經被取消了
* 如果成功,則該節點的狀態ws被改為0了
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* 通過enq方法將node自旋的方式加入同步佇列隊尾
* 這裡放回的p是node在同步佇列的前驅節點
*/
Node p = enq(node);
int ws = p.waitStatus;
// ws大於0 的情況只有 cancenlled,表示node的前驅節點取消了爭取鎖,那直接喚醒node執行緒
// ws <= 0 會使用cas操作將前驅節點的ws置為signal,如果cas失敗也會喚醒node
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
// 自旋的方式入隊
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
// 返回的是node的前驅節點
return t;
}
}
}
}
檢查中斷狀態
ok,一旦signal之後,節點被成功轉移到同步佇列後,這時下面這個迴圈就會退出了,繼續回到這裡:
int interruptMode = 0;
// 如果這個節點的執行緒不在同步佇列中,說明該執行緒還不具備競爭鎖的資格,會一直掛起
while (!isOnSyncQueue(node)) {
// 掛起執行緒
LockSupport.park(this);
// 如果執行緒中斷,退出
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
interruptMode可以有以下幾種取值:
/** await 返回的時候,需要重新設定中斷狀態 */
private static final int REINTERRUPT = 1;
/** await 返回的時候,需要丟擲 InterruptedException 異常 */
private static final int THROW_IE = -1;
/** interruptMode取0的時候表示在await()期間,沒有發生中斷 */
說到這裡我們需要明白,LockSupport.park(this)
掛起的執行緒是什麼時候喚醒的:
- signal方法將節點轉移到同步佇列中,且獲取到了鎖或者對前驅節點的cas操作失敗,呼叫了
LockSupport.unpark(node.thread);
方法。 - 在park的時候,另外一個執行緒對掛起的執行緒進行了中斷。
喚醒之後,我們可以看到呼叫checkInterruptWhileWaiting方法檢查等待期間是否發生了中斷,如果不為0表示確實在等待期間發生了中斷。
但其實這個方法的返回結果用interruptMode變數接收,擁有更加豐富的內涵,它還能夠判斷中斷的時機是否在signal之前。
int checkInterruptWhileWaiting(Node node)
該方法用於判斷該執行緒是否在掛起期間發生了中斷。
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?// 如果處於中斷狀態,返回true,且將重置中斷狀態
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :// 如果中斷了,判斷何時中斷
0; // 沒有中斷, 返回0
}
boolean transferAfterCancelledWait(Node node)
該方法判斷何時中斷,是否在signal之前。
final boolean transferAfterCancelledWait(Node node) {
// 嘗試使用CAS操作將node 的ws設定為0
// 如果成功,說明在signal方法之前中斷就已經發生:
// 原因在於:signal如果在此之前發生,必然已經cas操作將ws設定為0了,這裡不可能設定成功
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 就算中斷了,也將節點入隊
enq(node);
return true;
}
/*
* If we lost out to a signal(), then we can't proceed
* until it finishes its enq(). Cancelling during an
* incomplete transfer is both rare and transient, so just
* spin.
* 這裡就是signal之後發生的中斷
* 但是signal可能還在進行轉移中,這邊自旋等一下它完成
*/
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
這裡的話,我們還是稍微總結一下:
- await()中的節點中斷之後,被喚醒有多種情況:
- 無中斷的情況:signal方法成功將節點移入同步佇列且節點成功獲取資源,喚醒該執行緒,此時退出的時候interruptMode為0。
- 有中斷的情況:
- signal之前中斷,interruptMode設定為THROW_IE。
- signal之後中斷,interruptMode設定為REINTERRUPT。
- 中斷時,無論signal之前或之後,節點無論如何都會進入阻塞佇列。
處理中斷狀態
接下來三個部分我將一一說明:
// 第一部分
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 第二部分
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // 清除取消的節點
// 第三部分
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
第一部分
signal喚醒的執行緒並不會立即獲取到資源,從while迴圈退出後,會通過acquireQueued方法加入獲取同步狀態的競爭中。
// 第一部分
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
acquireQueued(node, savedState)
中node此時已經被加入同步佇列了,savedState是之前儲存的state。
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);
p.next = null; // help GC
failed = false;
return interrupted; //
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued方法返回時,表示已經獲取到了鎖,且返回的是interrupted值,如果返回true,表示已經被中斷。
接著判斷interruptMode != THROW_IE
表示是在signal之後發生的中斷,需要重新中斷當前執行緒,將interruptMode設定為REINTERRUPT。
第二部分
// 第二部分
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters(); // 清除取消的節點
前面說了,signal會將節點移到同步佇列中,最後一步需要和條件佇列斷開關係,也就是:node.nextWaiter = null
,但這是想象中比較正常的情況,如果在signal之前被中斷,節點也會被加入同步佇列中,這時其實是沒有呼叫這個斷開關係的。
因此這邊做一點處理, unlinkCancelledWaiters()
邏輯上面也說過了,可以回過頭去看看,主要是清除佇列中已經取消等待的節點。
第三部分
最後一個部分,就是對兩種interruptMode的情況進行處理,看看程式碼就知道了:
void reportInterruptAfterWait(interruptMode)
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
// signal 之前的中斷, 需要丟擲異常
if (interruptMode == THROW_IE)
throw new InterruptedException();
// signal 之後發生的中斷, 需要重新中斷
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
帶超機制的void await()
帶超時機制的await()方法有以下幾個,簡單看下即可:
- long awaitNanos(long nanosTimeout)
- boolean awaitUntil(Date deadline)
- boolean await(long time, TimeUnit unit)
我們選最後一個來看看,主要看看和之前await()方法不一樣的地方:
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
// 計算等待的時間
long nanosTimeout = unit.toNanos(time);
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
// 截止時間
final long deadline = System.nanoTime() + nanosTimeout;
// 表示是否超時
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 等待時間到了
if (nanosTimeout <= 0L) {
// 這個方法返回true表示在這個方法內,已經將節點轉移到阻塞佇列中
// 返回false,表示signal已經發生,表示沒有超時
timedout = transferAfterCancelledWait(node);
break;
}
//spinForTimeoutThreshold 是AQS中的一個欄位,如果超過1ms,使用parkNonos
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
// 更新一下還需要等待多久
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout;
}
不丟擲InterruptedException的await
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted())
interrupted = true;
}
// 相比await() 針對中斷少了丟擲異常的操作,而是直接進行中斷
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
Condition的使用
最後以一個Java doc給的例子結尾吧:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 依賴於 lock 來產生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生產
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 佇列已滿,等待,直到 not full 才能繼續生產
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 生產成功,佇列已經 not empty 了,發個通知出去
} finally {
lock.unlock();
}
}
// 消費
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 佇列為空,等待,直到佇列 not empty,才能繼續消費
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 被我消費掉一個,佇列 not full 了,發個通知出去
return x;
} finally {
lock.unlock();
}
}
}
其實這個之前也說過,ArrayBlockingQueue就是採用了這種方式實現的生產者-消費者模式,如果你感興趣,可以看看具體的實現細節哦。
總結
- Condition的await()和signal()基於Lock,相比於基於Object的wait()和notify()方法,它提供更加靈活的等待通知的機制。
- 支援豐富的功能如:帶超時機制的await(),不響應中斷的await(),以及多個等待的條件佇列。
- Condition的await()方法會將執行緒包裝為等待節點,加入等待佇列中,並將AQS同步佇列中的節點移除,接著不斷檢查
isOnSyncQueue(Node node)
,如果在等待佇列中,就一直等著,如果signal將它移到AQS佇列中,則退出迴圈。 - Condition的signal()方法則是先檢查當前執行緒是否獲取了鎖,接著將等待佇列中的節點通過Node的操作直接加入AQS佇列。執行緒並不會立即獲取到資源,從while迴圈退出後,會通過acquireQueued方法加入獲取同步狀態的競爭中。
參考閱讀
- java Condition原始碼分析
- 一行一行原始碼分析清楚 AbstractQueuedSynchronizer (二)
- 【死磕 Java 併發】—– J.U.C 之 Condition
- 方騰飛:《Java併發程式設計的藝術》
- DougLea : 《Java併發程式設計實戰》