Condition隊列原理分析
前言
每一個Java對象都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、 wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現線程之間的通信(等待/通知)機制。
在前一篇文章中我們介紹了Lock對象的實現類ReentrantLock和AQS隊列實現原理,而Lock也有自己對應的等待/通知機制Condition隊列,Condition接口也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,主要通過方法await()和singal()實現。
在學習本篇文章之前,建議先去學一下上一篇文章介紹的ReentrantLock和AQS隊列實現原理。因爲本文的內容也離不開AQS和Node對象。
初識Condition
Condition和Lock一樣,也是JUC內的一個接口。Condition接口定義了等待/通知兩種類型的方法,當前線程調用這些方法時,需要提前獲取到 Condition對象關聯的鎖。Condition對象是由Lock對象(調用Lock對象的newCondition()方法)創建出來的,換句話說,Condition是依賴Lock對象的。
Condition的實現類ConditionObject也是AQS類中的一個內內部類,也依賴於Node對象。
Condition使用示例
Condition的使用也非常簡單,下面是一個簡單的使用示例:
package com.zwx.concurrent.lock;
import java.util.Locale;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockConditionDemo {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
new Thread(new ConditionAwait(lock,condition)).start();
Thread.sleep(1000);
new Thread(new ConditionSingal(lock,condition)).start();
}
}
class ConditionAwait implements Runnable{
private Lock lock;
private Condition condition;
public ConditionAwait(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("await begin");
try {
lock.lock();
condition.await();
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();
}
System.out.println("await end");
}
}
class ConditionSingal implements Runnable{
private Lock lock;
private Condition condition;
public ConditionSingal(Lock lock, Condition condition) {
this.lock = lock;
this.condition = condition;
}
@Override
public void run() {
System.out.println("signal begin");
try {
lock.lock();
condition.signal();
}finally {
lock.unlock();
}
System.out.println("signal end");
}
}
運行之後,輸出結果爲:
這個效果就是和wait(),nodity()一樣的,那麼Condition中的等待通知機制是如何實現的呢?
Condition原理分析
Condition接口的實現類ConditionObject是一個多線程協調通信的工具類,可以讓線程一起等待某個條件(condition),只有滿足條件時,線程纔會被喚醒。
和上一篇文章介紹的AQS同步隊列類似,Condition也是一個依賴Node對象構建的FIFO隊列。
Condition隊列,稱之爲等待隊列,和AQS隊列不同的是,Condition等待隊列不會維護prev和next,維護的只是一個單項列表,通過firstWaiter和lastWaiter實現頭尾節點,然後除了lastWaiter節點,其餘每個節點會有一個nextWaiter指向下一個節點,Condition隊列大致示意圖如下:
condition.wait()源碼解讀
接下來讓我們進入源碼層面開始剖析condition的實現原理。上文的示例中,當我們調用condition.wait()時,我們進入AbstractQueuedSynchronizer類中的await()方法。
AQS#await()
第一步是檢測是否被中斷,這個就不用多說,我們看下面的addConditionWaiter()方法:
AQS#addConditionWaiter()
爲了便於理解,我們還是把Node對象貼出來看一看:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;//表示當前線程狀態是取消的
static final int SIGNAL = -1;//表示當前線程正在等待鎖
static final int CONDITION = -2;//Condition隊列初始化Node節點時的默認狀態
static final int PROPAGATE = -3;//CountDownLatch等工具中使用到,暫時用不到
volatile int waitStatus;//Node節點中線程的狀態,AQS隊列中默認爲0
volatile Node prev;//當前節點的前一個節點
volatile Node next;//當前節點的後一個節點
volatile Thread thread;//當前節點封裝的線程信息
Node nextWaiter;//Condition隊列維護
final boolean isShared() {//暫時用不到
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {//獲取當前節點的上一個節點
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {//構造一個節點:addWaiter方法中會使用,此時waitStatus默認等於0
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { //構造一個節點:Condition中會使用
this.waitStatus = waitStatus;
this.thread = thread;
}
}
需要說明的是,AQS隊列中初始化Node節點的時候不會傳入狀態,所以默認爲0,然後我們之前分析的時候知道,中途會被改爲1,然後線程異常時候會有3出現,所以AQS隊列中的Node節點實際上只會有-1,0,1三種狀態,而Condition隊列,初始化的時候調用的是另一個構造器,直接傳入了-2狀態,所以不會有0這個默認狀態,故而Condition隊列中只會有-2和1兩種狀態。
這裏刪除無效節點的方法我們後面再分析,我們現在假設有線程A和線程B,線程A進來的時候因爲Condition隊列還沒被初始化,所以執行的是1879和1882兩行代碼,這時候就構建出了這樣的一個Condition隊列:
這時候因爲只有一個節點,所以firstWaiter和lastWaiter都是指向同一個節點,而ThreadA節點中這時候nextWaiter是空的,因爲這時候還沒有下一個節點。
這時候線程B也進來,那麼就會加入到已經構建好的對象(注意,這兩個線程必須共用一個Lock對象,否則會構建不同的Condition隊列),ThreadB進來就會執行1881和1882兩行代碼,最終得到下面的Condition隊列:
Condition構建好了,先不管Node節點狀態是怎麼變成1(cancle)的,我們假如線程B的節點狀態變成1了,然後進入unlinkCancelledWaiters()方法看看是怎麼移除無效節點的,當ThreadB狀態爲1,得到如下Condition隊列(和上圖唯一的區別就是ThreadB所在Node狀態變成了1):
AQS#unlinkCancelledWaiters()
這個方法的邏輯也不算難,只要記住兩個屬性:
一個是t,t是需要循環的節點,第一次是firstwaiter,循環完了之後就會把nextWaiter賦值給t繼續循環(1933和1945兩行代碼);
另一個是trail,用來記錄已經循環過的節點,循環的時候如果沒有取消的節點,那就是把t循環完之後賦值給trail,然後繼續循環
這裏我們還是繼續演示一下,第一次循環肯定肯定走的是1944行代碼和1945行代碼,因爲firstWaiter肯定不爲空,狀態也等於Node.CONDITION,循環結束之後會得到如下結果:t=ThreadB,trail=firstWaiter;
然後繼續循環,這時候因爲t狀態是1,所以if條件成立,進入1935行開始執行清除無效節點的邏輯,t.nextWaiter = null;因爲當前ThreadB是尾節點,所以這種情況這句話是不起什麼作用的,針對非尾節點,纔會有作用。
又因爲trail=firstWaiter不等於null,所以會執行1939行代碼(else分支),這時候因爲ThreadB線程已經沒有下一個節點了,所以1939行相當於:trail.nextWaiter = null;因爲trail=firstWaiter,所以等價於:firstWaiter.nextWaiter=null,於是得到下面的最新Condition隊列:
然後執行lastWaiter = trail;等價於lastWaiter = firster;得到如下Condition隊列:
可以看到ThreadB這個無效節點已經被清除了。
忘掉這個清除無效節點邏輯,回到我們的正常邏輯,隊列構建完成之後,await()方法會繼續往下面執行:
接下來回去執行釋放鎖fullyRelease(Node)的邏輯,因爲線程await()方法本來就是要把當前鎖讓給另一個線程,所以肯定要釋放鎖,要不然其他線程不可能獲得鎖。
AQS#fullyRelease(Node)
這裏首先會獲取到當前的狀態,然後把狀態傳入elease()方法,前面介紹ReentrantLock的時候,lock.unlock()也會調用這個release(arg)方法,只不過unlock()是固定傳的1,也就是說如果有重入調用一次只會state-1,而這裏是直接全部被減去。
這裏就不在介紹release(arg)方法了,沒有了解過的可以看我前面介紹ReentrantLock和AQS的文章。
這裏如果釋放鎖成功之後,又會繼續回到我們的await()方法:
這時候會繼續去執行while循環中的isOnSyncQueue方法,這個方法的意思是判斷一下當前線程所在的Node是不是在AQS同步隊列,那麼爲什麼要有這個判斷?
大家注意了,這是在併發場景下,所以也可能會有其他線程已經把線程B喚醒了,喚醒之後並不是說就能直接獲得鎖,而是會去爭搶鎖,那麼爭搶鎖失敗了就會加入到AQS同步隊列當中,所以這裏要有這個判斷,如果不在AQS同步隊列,那就可以把當前線程掛起了。
AQS#isOnSyncQueue(Node)
這裏有一個點需要特別指出的是,Condition隊列的節點,當被其他線程調用了singal()方法喚醒的時候,就需要去爭搶鎖,而爭搶鎖失敗就有可能被加入到AQS同步隊列,所以這裏纔會有prev和next屬性的判斷
還有一個點如果大家不記得之前構造AQS同步隊列的邏輯可能就不太好理解,爲了便於大家理解,我把上文介紹AQS同步隊列中的enq代碼片段貼過來解釋一下就很好理解了:
上面代碼中如果597行成功,而598行的CAS失敗,那麼這時候node.prev!=null,但是他替換tail節點失敗了,所以等於是沒有加入到AQS同步隊列,所以上面即使node.prev!=null,仍然需要從tail節點遍歷一下來確定。
AQS#findNodeFromTail(Node)
這段代碼應該很好理解,就不多做解釋了。
回到await()主方法:
到這裏,我們的線程B進來的時候肯定是不會在AQS同步隊列中的,搜易進入下一行,當前線程被park()掛起。掛起之後需要等到其他線程調用singal()方法喚醒。
condition.signal()源碼解讀
上文的示例中,當我們調用condition.signal()時,我們進入AbstractQueuedSynchronizer類中的signal()方法。
AQS#signal()
這個方法比較簡單,只是做了個簡單的判斷,我們進入doSignal(Node)方法看看具體是如何喚醒其他線程的。
AQS#doSignal(Node)
循環體中主要是判斷當前Condition隊列中第二個節點是否可用,如果可以用,就剔除掉。
而主要的邏輯在while條件當中的transferForSignal(Node),這個就是singal操作的核心代碼了,主要就是將Condition隊列中的Node轉移到AQS同步隊列當中去競爭鎖。
這裏經過一次do操作之後實際上已經把原先的firstWaiter節點移除了,因爲線程被喚醒後需要加入到AQ同步隊列當中,先把Node移出Condition,後面再調用transferForSignal方法加入AQS同步隊列:
注意了,線程被sigal喚醒後並不是說就能直接獲得鎖,還是需要通過競爭纔可以獲得鎖,所以需要將其轉移到AQS同步隊列去爭搶鎖。
AQS#transferForSignal(Node)
這裏註釋上都寫明瞭大致意思,應該能看的懂,期中enq方法就是將Node節點加入到AQS同步隊列的邏輯,而1710到1712行代碼不要也是可以的,因爲我們在lock.lock()和lock.unlock()的時候都有剔除無效節點的操作,這裏這麼做的考慮之一,是可以提升一定的性能,我們假設這個AQS同步隊列當中原先只有一個節點(除了head哨兵節點),那麼這時候p(即原先的tail)節點是無效節點,這時候重新喚醒當前節點去搶佔鎖,而這時候之前持有鎖的線程恰巧釋放了鎖,那麼他就有可能直接搶佔成功了。
回到AQS#await()
上面我們的線程被掛在了上面的2062行,但是要注意,這裏被喚醒有兩種情況:
- 被singal()方法喚醒
- 被interrupt()中斷
所以喚醒之後第一件事就是要判斷到底是被interrupt()喚醒的還是被singal()喚醒的。
AQS#checkInterruptWhileWaiting(Node)
transferAfterCancelledWait(Node)方法主要就是判斷到底是情況2還是情況3。
AQS#checkInterruptWhileWaiting(Node)
上面我們可以知道線程恢復到底是先interrupt()還是先singal(),返回之後回到之前的方法
繼續回到AQS#await()
到這裏我們的真個流程分析基本上結束了,後面的acquireQueued方法就是搶佔鎖了,搶佔鎖的時候如果被中斷了纔會返回true,所以這裏的判斷針對的就是如果搶佔鎖被中斷了,而上面的interruptMode=0的情況,我們需要改爲REINTERRUPT。再往後就是清除取消的節點,以及根據interruptMode來響應中斷了,reportInterruptAfterWait方法也非常簡單:
總結
Condition隊列和AQS同步隊列中的節點共用的是Node對象,通過不同狀態來區分,而一個Node同一時間只能存在於一個隊列,一個Node從Condition隊列移出加入到AQS同步隊列的流程圖如下:
後面將會繼續分析JUC中的其他工具的實現原理,感興趣的 請關注我,和孤狼一起學習進步。