【併發編程系列6】Condition隊列原理及await和singal(等待/喚醒)機制源碼分析

前言

每一個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)

這個方法中第一個判斷在上面transferForSignal(Node)中的已經有一個同樣的CAS操作了,所以如果當前線程是被singal喚醒的,那麼這個CAS一定會失敗,所以只有被interrupt中斷了,這裏的CAS纔會成功,成功後執行
上面我們可以知道線程恢復到底是先interrupt()還是先singal(),返回之後回到之前的方法

繼續回到AQS#await()

在這裏插入圖片描述
到這裏我們的真個流程分析基本上結束了,後面的acquireQueued方法就是搶佔鎖了,搶佔鎖的時候如果被中斷了纔會返回true,所以這裏的判斷針對的就是如果搶佔鎖被中斷了,而上面的interruptMode=0的情況,我們需要改爲REINTERRUPT。再往後就是清除取消的節點,以及根據interruptMode來響應中斷了,reportInterruptAfterWait方法也非常簡單:
在這裏插入圖片描述

總結

Condition隊列和AQS同步隊列中的節點共用的是Node對象,通過不同狀態來區分,而一個Node同一時間只能存在於一個隊列,一個Node從Condition隊列移出加入到AQS同步隊列的流程圖如下:
在這裏插入圖片描述
後面將會繼續分析JUC中的其他工具的實現原理,感興趣的 請關注我,和孤狼一起學習進步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章