AQS中的Condition是什么?
歡迎關(guān)注:王有志
期待你加入Java人的提桶跑路群:共同富裕的Java人
今天來和大家聊聊Condition,Condition為AQS“家族”提供了等待與喚醒的能力,使AQS"家族"具備了像synchronized一樣暫停與喚醒線程的能力。我們先來看兩道關(guān)于Condition的面試題目:
- Condition和Object的等待與喚醒有什么區(qū)別?
- 什么是Condition隊列?
接下來,我們就按照“是什么”,“怎么用”和“如何實(shí)現(xiàn)”的順序來揭開Condition的面紗吧。
Condition是什么?
Condition是Java中的接口,提供了與Object#wait和Object#notify相同的功能。Doug Lea在Condition接口的描述中提到了這點(diǎn):
Conditions (also known as condition queues or condition variables) provide a means for one thread to suspend execution (to “wait”) until notified by another thread that some state condition may now be true.
來看Condition接口中提供了哪些方法:
public interface Condition {void await() throws InterruptedException;void awaitUninterruptibly();long awaitNanos(long nanosTimeout) throws InterruptedException;boolean await(long time, TimeUnit unit) throws InterruptedException;boolean awaitUntil(Date deadline) throws InterruptedException;void signal();void signalAll(); }Condition只提供了兩個功能:等待(await)和喚醒(signal),與Object提供的等待與喚醒時相似的:
public final void wait() throws InterruptedException;public final void wait(long timeoutMillis, int nanos) throws InterruptedException;public final native void wait(long timeoutMillis) throws InterruptedException;@HotSpotIntrinsicCandidate public final native void notify();@HotSpotIntrinsicCandidate public final native void notifyAll();喚醒功能上,Condition與Object的差異并不大:
- Condition#signal ≈ \approx ≈ Object#notify
- Condition#signalAll = = = Object#notifyAll
多個線程處于等待狀態(tài)時,Object#notify()是“隨機(jī)”喚醒線程,而Condition#signal則由具體實(shí)現(xiàn)決定如何喚醒線程,如:ConditionObject喚醒的是最早進(jìn)入等待的線程,但兩個方法均只喚醒一個線程。
等待功能上,Condition與Object的共同點(diǎn)是:都會釋放持有的資源,Condition釋放鎖,Object釋放Monitor,即進(jìn)入等待狀態(tài)后允許其他線程獲取鎖/監(jiān)視器。主要的差異體現(xiàn)在Condition支持了更加豐富的場景,通過一張表格來對比下:
| Condition#await() | Object#wait() | 暫停線程,拋出線程中斷異常 |
| Condition#awaitUninterruptibly() | / | 暫停線程,不拋出線程中斷異常 |
| Condition#await(time, unit) | Object#wait(timeoutMillis, nanos) | 暫停線程,直到被喚醒或等待指定時間后,超時后自動喚醒返回false,否則返回true |
| Condition#awaitUntil(deadline) | / | 暫停線程,直到被喚醒或到達(dá)指定時間點(diǎn),超時后自動喚醒返回false,否則返回true |
| Condition#awaitNanos(nanosTimeout) | / | 暫停線程,直到被喚醒或等待指定時間后,返回值表示被喚醒時的剩余時間(nanosTimeout-耗時),結(jié)果為負(fù)數(shù)表示超時 |
除了以上差異外,Condition還支持創(chuàng)建多個等待隊列,即同一把鎖擁有多個等待隊列,線程在不同隊列中等待,而Object只有一個等待隊列?!禞ava并發(fā)編程的藝術(shù)》中也有一張類似的表格,放在這里供大家參考:
Tips:
- 實(shí)際上signal翻譯為喚醒并不恰當(dāng)~~
- 涉及到Condition的實(shí)現(xiàn)部分,下文通過AQS中的ConditionObject詳細(xì)解釋。
Condition怎么用?
既然Condition與Object提供的等待與喚醒功能相同,那么它們的用法是不是也很相似呢?
與調(diào)用Object#wait和Object#notifyAll必須處于synchronized修飾的代碼中一樣(獲取Monitor),調(diào)用Condition#await和Condition#signalAll的前提是要先獲取鎖。但不同的是,使用Condition前,需要先通過鎖去創(chuàng)建Condition。
以ReentrantLock中提供的Condition為例,首先是創(chuàng)建Condition對象:
ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition();然后是獲取鎖并調(diào)用await方法:
new Thread(() -> {lock.lock();try {condition.await();} catch (InterruptedException e) {throw new RuntimeException(e);}lock.unlock(); }最后,通過調(diào)用singalAll喚醒全部阻塞中的線程:
new Thread(() -> {lock.lock();condition.signalAll();lock.unlock(); }ConditionObject的源碼分析
作為接口Condition非常慘,因為在Java中只有AQS中的內(nèi)部類ConditionObject實(shí)現(xiàn)了Condition接口:
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {public class ConditionObject implements Condition, java.io.Serializable {private transient Node firstWaiter;private transient Node lastWaiter;}static final class Node {// 省略} }ConditionObject只有兩個Node類型的字段,分別是鏈?zhǔn)浇Y(jié)構(gòu)中的頭尾節(jié)點(diǎn),ConditionObject就是通過它們實(shí)現(xiàn)的等待隊列。那么ConditionObject的等待隊列起到了怎樣的作用呢?是類似于AQS中的排隊機(jī)制嗎?帶著這兩個問題,我們正是開始源碼的分析。
await方法的實(shí)現(xiàn)
Condition接口中定義了4個線程等待的方法:
- void await() throws InterruptedException
- void awaitUninterruptibly();
- long awaitNanos(long nanosTimeout) throws InterruptedException;
- boolean await(long time, TimeUnit unit) throws InterruptedException;
- boolean awaitUntil(Date deadline) throws InterruptedException;
方法雖然很多,但它們之間的差異較小,只體現(xiàn)在時間的處理上,我們看其中最常用的方法:
public final void await() throws InterruptedException {// 線程中斷,拋出異常if (Thread.interrupted()) {throw new InterruptedException();}// 注釋1:加入到Condition的等待隊列中Node node = addConditionWaiter();// 注釋2:釋放持有鎖(調(diào)用AQS的release)int savedState = fullyRelease(node);int interruptMode = 0;// 注釋3:判斷是否在AQS的等待隊列中while (!isOnSyncQueue(node)) {LockSupport.park(this);// 中斷時退出方法if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) {break;}}// 加入到AQS的等待隊列中,調(diào)用AQS的acquireQueued方法if (acquireQueued(node, savedState) && interruptMode != THROW_IE) {interruptMode = REINTERRUPT;}// 斷開與Condition隊列的聯(lián)系if (node.nextWaiter != null) {unlinkCancelledWaiters();}if (interruptMode != 0) {reportInterruptAfterWait(interruptMode);} }注釋1的部分,調(diào)用addConditionWaiter方法添加到Condition隊列中:
private Node addConditionWaiter() {// 判斷當(dāng)前線程是否為持有鎖的線程if (!isHeldExclusively()) {throw new IllegalMonitorStateException();}// 獲取Condition隊列的尾節(jié)點(diǎn)Node t = lastWaiter;// 斷開不再位于Condition隊列的節(jié)點(diǎn)if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;}// 創(chuàng)建Node.CONDITION模式的Node節(jié)點(diǎn)Node node = new Node(Node.CONDITION);if (t == null) {// 隊列為空的場景,將node設(shè)置為頭節(jié)點(diǎn)firstWaiter = node;} else {// 隊列不為空的場景,將node添加到尾節(jié)點(diǎn)的后繼節(jié)點(diǎn)上t.nextWaiter = node;}// 更新尾節(jié)點(diǎn)lastWaiter = node;return node; }可以看到,Condition的隊列是一個樸實(shí)無華的雙向鏈表,每次調(diào)用addConditionWaiter方法,都會加入到Condition隊列的尾部。
注釋2的部分,釋放線程持有的鎖,同時移出AQS的隊列,內(nèi)部調(diào)用了AQS的release方法:
=final int fullyRelease(Node node) {try {int savedState = getState();if (release(savedState)) {return savedState;}throw new IllegalMonitorStateException();} catch (Throwable t) {node.waitStatus = Node.CANCELLED;throw t;} }因為已經(jīng)分析過AQS的release方法和ReentrantLock實(shí)現(xiàn)的tryRelease方法,這里我們就不過多贅述了。
注釋3的部分,isOnSyncQueue判斷當(dāng)前線程是否在AQS的等待隊列中,我們來看此時存在的情況:
- 如果isOnSyncQueue返回false,即線程不在AQS的隊列中,進(jìn)入自旋,調(diào)用LockSupport#park暫停線程;
- 如果isOnSyncQueue返回true,即線程在AQS的隊列中,不進(jìn)入自旋,執(zhí)行后續(xù)邏輯。
結(jié)合注釋1和注釋2的部分,Condition#await的實(shí)現(xiàn)原理了就很清晰了:
- Condition與AQS分別維護(hù)了一個等待隊列,而且是互斥的,即同一個節(jié)點(diǎn)只會出現(xiàn)在一個隊列中;
- 當(dāng)調(diào)用Condition#await時,將線程添加到Condition的隊列中(注釋1),同時從AQS隊列中移出(注釋2);
- 接著判斷線程位于的隊列:
- 位于Condition隊列中,該線程需要被暫停,調(diào)用LockSupport#park;
- 位于AQS隊列中,該線程正在等待獲取鎖。
基于以上的結(jié)論,我們已經(jīng)能夠猜到喚醒方法Condition#signalAll的原理了:
- 將線程從Condition隊列中移出,并添加到AQS的隊列中;
- 調(diào)用LockSupport.unpark喚醒線程。
至于這個猜想是否正確,我們接著來看喚醒方法的實(shí)現(xiàn)。
Tips:如果忘記了AQS中相關(guān)方法是如何實(shí)現(xiàn)的,可以回顧下《AQS的今生,構(gòu)建出JUC的基礎(chǔ)》。
signal和signalAll方法的實(shí)現(xiàn)
來看signal和signalAll的源碼:
// 喚醒一個處于等待中的線程 public final void signal() {if (!isHeldExclusively()) {throw new IllegalMonitorStateException();}// 獲取Condition隊列中的第一個節(jié)點(diǎn)Node first = firstWaiter;if (first != null) {// 喚醒第一個節(jié)點(diǎn)doSignal(first);} }// 喚醒全部處于等待中的線程 public final void signalAll() {if (!isHeldExclusively()){throw new IllegalMonitorStateException();}Node first = firstWaiter;if (first != null) {// 喚醒所有節(jié)點(diǎn)doSignalAll(first);} }兩個方法唯一的差別在于頭節(jié)點(diǎn)不為空的場景下,是調(diào)用doSignal喚醒一個線程還是調(diào)用doSignalAll喚醒所有線程:
private void doSignal(Node first) {do {// 更新頭節(jié)點(diǎn)if ( (firstWaiter = first.nextWaiter) == null) {// 無后繼節(jié)點(diǎn)的場景l(fā)astWaiter = null;}// 斷開節(jié)點(diǎn)的連接first.nextWaiter = null;// 喚醒頭節(jié)點(diǎn)} while (!transferForSignal(first) && (first = firstWaiter) != null); }private void doSignalAll(Node first) {// 將Condition的隊列置為空lastWaiter = firstWaiter = null;do {// 斷開鏈接Node next = first.nextWaiter;first.nextWaiter = null;// 喚醒當(dāng)前頭節(jié)點(diǎn)transferForSignal(first);// 更新頭節(jié)點(diǎn)first = next;} while (first != null); }可以看到,無論是doSignal還是doSignalAll都只是將節(jié)點(diǎn)移出Condition隊列,而真正起到喚醒作用的是transferForSignal方法,從方法名可以看到該方法是通過“轉(zhuǎn)移”進(jìn)行喚醒的,我們來看源碼:
final boolean transferForSignal(Node node) {// 通過CAS替換node的狀態(tài)// 如果替換失敗,說明node不處于Node.CONDITION狀態(tài),不需要喚醒if (!node.compareAndSetWaitStatus(Node.CONDITION, 0)) {return false;}// 將節(jié)點(diǎn)添加到AQS的隊列的隊尾// 并返回老隊尾節(jié)點(diǎn),即node的前驅(qū)節(jié)點(diǎn)Node p = enq(node);int ws = p.waitStatus;// 對前驅(qū)節(jié)點(diǎn)狀態(tài)的判斷if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL)) {LockSupport.unpark(node.thread);}return true; }transferForSignal方法中,調(diào)用enq方法將node重新添加到AQS的隊列中,并返回node的前驅(qū)節(jié)點(diǎn),隨后對前驅(qū)節(jié)點(diǎn)的狀態(tài)進(jìn)行判斷:
- 當(dāng) w s > 0 ws > 0 ws>0時,前驅(qū)節(jié)點(diǎn)處于Node.CANCELLED狀態(tài),前驅(qū)節(jié)點(diǎn)退出鎖的爭搶,node可以直接被喚醒;
- 當(dāng) w s ≤ 0 ws \leq 0 ws≤0時,通過CAS修改前驅(qū)節(jié)點(diǎn)的狀態(tài)為Node.SIGNAL,設(shè)置失敗時,直接喚醒node。
《AQS的今生,構(gòu)建出JUC的基礎(chǔ)》中介紹了waitStatus的5種狀態(tài),其中Node.SIGNAL狀態(tài)表示需要喚醒后繼節(jié)點(diǎn)。另外,在分析shouldParkAfterFailedAcquire方法的源碼時,我們知道在進(jìn)入AQS的等待隊列時,需要將前驅(qū)節(jié)點(diǎn)的狀態(tài)更新為Node.SIGNAL。
最后來看enq的實(shí)現(xiàn):
private Node enq(Node node) {for (;;) {// 獲取尾節(jié)點(diǎn)Node oldTail = tail;if (oldTail != null) {// 更新當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)node.setPrevRelaxed(oldTail);// 更新尾節(jié)點(diǎn)if (compareAndSetTail(oldTail, node)) {oldTail.next = node;// 返回當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)(即老尾節(jié)點(diǎn))return oldTail;}} else {initializeSyncQueue();}} }enq的實(shí)現(xiàn)就非常簡單了,通過CAS更新AQS的隊列尾節(jié)點(diǎn),相當(dāng)于添加到AQS的隊列中,并返回尾節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)。好了,喚醒方法的源碼到這里就結(jié)束了,是不是和我們當(dāng)初的猜想一模一樣呢?
圖解ConditionObject原理
功能上,Condition實(shí)現(xiàn)了AQS版Object#wait和Object#notify,用法上也與之相似,需要先獲取鎖,即需要在lock與unlock之間調(diào)用。原理上,簡單來說就是線程在AQS的隊列和Condition的隊列之間的轉(zhuǎn)移。
線程t持有鎖
假設(shè)有線程t已經(jīng)獲取了ReentrantLock,線程t1,t2和t3正在AQS的隊列中等待,我們可以得到這樣的結(jié)構(gòu):
線程t執(zhí)行Condition#await
如果線程t中調(diào)用了Condition#await方法,線程t進(jìn)入Condition的等待隊列中,線程t1獲取ReentrantLock,并從AQS的隊列中移出,結(jié)構(gòu)如下:
線程t1執(zhí)行Condition#await
如果線程t1中也執(zhí)行了Condition#await方法,同樣線程t1進(jìn)入Condition隊列中,線程t2獲取到ReentrantLock,結(jié)構(gòu)如下:
線程t2執(zhí)行Condition#signal
如果線程t2執(zhí)行了Condition#signal,喚醒Condition隊列中的第一個線程,此時結(jié)構(gòu)如下:
通過上面的流程,我們就可以得到線程是如何在Condition隊列與AQS隊列中轉(zhuǎn)移的:
結(jié)語
關(guān)于Condition的內(nèi)容到這里就結(jié)束了,無論是理解,使用還是剖析原理,Condition的難度并不高,只不過大家可能平時用得比較少,因此多少有些陌生。
最后,截止到文章發(fā)布,我應(yīng)該是把開頭兩道題目的題解寫完了吧~~
好了,今天就到這里了,Bye~~
總結(jié)
以上是生活随笔為你收集整理的AQS中的Condition是什么?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 4月雪
- 下一篇: 干程序 身体才是本钱!