06 ReentrantLock之Condition

我只是有點困呦 發表於 2020-09-23

1 Condition的基本使用

在前面學習synchronized 的時候,有講到 wait/notify的基本使用,結合 synchronized可以實現對執行緒間的通訊,JUC併發包裡面提供的同樣的功能。

Condition 是一個多執行緒協調通訊的工具類裡面提供了await/signal方法,可以讓某些執行緒一起等待某個條件(condition),只有滿足條件時,執行緒才會被喚醒。

1.1 簡單案例

public class LockConditionDemo {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread(() -> {
            System.out.println("threadA start");
            lock.lock();
            System.out.println("threadA getLock Running");
            try {
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.unlock();
            System.out.println("threadA end");
        });

        Thread threadB = new Thread(() -> {
            System.out.println("threadB start");
            lock.lock();
            System.out.println("threadB getLock Running");
            condition.signal();
            lock.unlock();
            System.out.println("threadB end");
        });

        threadA.start();
        TimeUnit.SECONDS.sleep(2);
        threadB.start();
    }
}

輸出結果如下:

threadA start
threadA getLock Running
threadB start
threadB getLock Running
threadB end
threadA end

1.2 簡單總結

當呼叫await方法後,當前執行緒會釋放鎖並等待,而其他執行緒呼叫condition 物件的 signal 或者 signalall 方法通知被阻塞的執行緒,然後自己執行 unlock 釋放鎖,被喚醒的執行緒獲得之前的鎖繼續執行,最後釋放鎖。

所以,condition 中兩個最重要的方法,一個是 await,一個是 signal 方法await:把當前執行緒阻塞掛起signal:喚醒阻塞的執行緒

2 Condition基本介紹

2.1 具體類圖的UML

image-20200921003258059

關於Condition 的實現類為 AbstractQueuedSynchronizer.ConditionObject 內部類。

先看下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();
}

ConditionObject的關鍵資料結構

private transient Node fristWaiter;

private transient Node lastWaiter;

每個ConditionObject,都維護著自己的條件等待佇列。

2.2 等待

原始碼分析之前,先介紹下等待佇列。

等待佇列是一個FIFO的佇列, 在佇列中的每個節點都包含了一個執行緒引用, 該執行緒就是在Condition物件上等待的執行緒, 如果一個執行緒呼叫了Condition.await() 方法, 那麼該執行緒將會釋放鎖、構造成節點加入等待佇列並進入等待狀態。一個Condtion包含一個等待佇列, Condtion擁有首節點(fristWaiter) 和尾節點(llastWaiter) 。當前執行緒呼叫Condition.await()方法, 將會以當前執行緒構造節點, 並將節點從尾部加入等待佇列。等待佇列的基本結構如下圖所示:

image-20200923235044450

一個Condition包含一個等待佇列, Condition擁有首節點(firstWaiter) 和尾節點(lastWaiter) 。當前執行緒呼叫Condition.await()方法, 將會以當前執行緒構造節點, 並將節點從尾部加入等待佇列。

2.2.1 await

呼叫condition.await()方法會使當前執行緒進入等待佇列並釋放鎖,同時執行緒變為等待狀態。當從await()方法返回時,一定是獲得與condition相關聯的鎖。

當呼叫condition.await()方法時,同步佇列的首節點(獲得鎖的節點)移動到condition的等待佇列。

呼叫該方法的執行緒成功獲取了鎖的執行緒,也就是同步佇列中的首節點,該方法會將當前執行緒構造成節點並加入等待佇列中,然後釋放同步狀態,喚醒同步佇列中的後繼節點,然後當前執行緒會進入等待狀態。當等待佇列中的節點被喚醒,則喚醒節點的執行緒開始嘗試獲取同步狀態。如果不是通過其他執行緒呼叫Condition.signal ()方法喚醒, 而是對等待執行緒進行中斷, 則會丟擲InterruptedException。如果從佇列的角度去看, 當前執行緒加入Condition的等待佇列, 該過程如下圖所示, 同步佇列的首節點並不會直接加入等待佇列, 而是通過addConditionWaiter()方法把當前執行緒構造成一個新的節點並將其加入等待佇列中。

image-20200922234438856

public final void await() throws InterruptedException {
    // 檢測當前執行緒的中斷標記,如果中斷位為1,則丟擲異常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 新增等待節點。就是一個簡單的連結串列維護節點的操作,具體參照addConditionWaiter講解
    Node node = addConditionWaiter();
    // 釋放佔有的鎖,並獲取當前鎖的state。並喚醒同步佇列中的一個執行緒
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 當前節點是否在同步佇列中,如果在同步阻塞佇列中,則申請鎖,去執行;
    // 同步佇列中的情況—併發:A執行緒await執行到此處釋放鎖。B執行緒singal後(等待節點加入同步節點),
    // 再執行下面的程式碼。acquireQueued可能失敗(再次park即可),也可能成功(B釋放lock鎖在此之前也執行了)
    // 如果不在同步佇列中(在等待佇列中),阻塞,等待滿足條件
    while (!isOnSyncQueue(node)) {
        // park阻塞,等待滿足條件
        LockSupport.park(this);
        // 執行緒從條件等待被喚醒後,執行緒要從條件佇列移除,進入到同步等待佇列。
        // 被喚醒有有如下兩種情況,一是條件滿足,收到singal訊號,二是執行緒被取消(中斷),
        // 如果是被中斷,需要根據不同模式,處理中斷。處理中斷,也有兩種方式:1.繼續設定中斷位;2:直接丟擲InterruptedException
        // 1、先singal,再interrupt再,重新中斷
        // 2、先interrupt再singal,直接丟擲異常
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
          	// 沒有中斷,由於從等待佇列加入到同步佇列。下次迴圈也會跳出此while
            break;
    }
    // singal喚醒後嘗試去申請鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 處理取消等待的節點,從等待佇列中移除
    if (node.nextWaiter != null)
        unlinkCancelledWaiters();
    // 根據中斷標識來進行中斷處理
  	// 如果是 THROW_IE,則丟擲中斷異常
    // 如果是 REINTERRUPT,則重新響應中斷
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

以上是await的整體方法,大致說明下流程。接下來會對每一個方法進行解析。

2.2.1.1 addConditionWaiter

新增條件等待節點

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果最後一個等待節點的狀態不是Node.CONDITION,則先刪除等待佇列中節點狀態不為Node.CONDITION的節點。
    // 有可能被中斷或者等待await到期
    if (t != null && t.waitStatus != Node.CONDITION) {
        // 等待佇列中移除取消等待的節點
        unlinkCancelledWaiters();
        // 清除不是Node.CONDITION的節點,尾結點有可能也會相應的改變
        t = lastWaiter;
    }
    // 當前執行緒構造節點並加入等待佇列
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    // 構造首節點
    if (t == null)
        firstWaiter = node;
    // 把當前等待執行緒構造為node加到尾結點後面,如之前的結構圖所示
    else
        t.nextWaiter = node;
    // 改變尾結點
    lastWaiter = node;
    return node;
}

2.2.1.2 unlinkCancelledWaiters

向等待佇列中的LastWaiter加入節點時,LastWaiter不是CONDITION狀態,從頭遍歷等待佇列中移除取消等待的節點。

private void unlinkCancelledWaiters() {
    // 首節點
    Node t = firstWaiter;
    // 從頭結點往後遍歷移除非Node.CONDITION節點
    // 若當前結點不被移除,就用trail臨時變數賦值,與下次不被取消的節點形成連結串列
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        // 移除佇列中不為Node.CONDITION的節點
        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;
    }
}

2.2.1.3 fullyRelease

徹底的釋放鎖,什麼叫徹底呢,就是如果當前鎖存在多次重入,那麼在這個方法中只需要釋放一次就會把所有的重入次數歸零。

釋放鎖,會喚醒同步佇列中的Head節點的下一個節點獲得鎖。此時同步佇列也會變化

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

2.2.1.4 isOnSyncQueue

如果不在同步佇列,說明當前節點沒有喚醒去爭搶同步鎖,所以需要把當前執行緒阻塞起來,直到其他的執行緒呼叫signal 喚醒

如果在同步佇列,意味著它需要去競爭同步鎖去獲得執行程式執行許可權。

等待佇列中的節點會重新加入到 同步佇列去競爭鎖。也就是當呼叫 signal的時候,會把當前節點從 等待佇列轉移到同步 佇列

final boolean isOnSyncQueue(Node node) {
    // 節點的狀態為Node.CONDITION 或 node.prev == null,表明該節點在等待佇列中,並沒有加入同步佇列。
    // 一個是根據waitStatus判斷,一個根據佇列的資料結構(同步對列為雙向連結串列,等待佇列為單向連結串列)。
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 同步佇列有pre與next(雙向連結串列)
    // 等待佇列為nextWaiter(單向連結串列)
    if (node.next != null) 
        return true;
    // node.prev不為空,但也不在同步佇列中,這個是由於CAS可能會失敗(同步佇列是先連結Pre節點,在CAS next節點)。
    // 為了不丟失訊號,從同步佇列中再次選擇該節點,如果找到則返回true,否則返回false
    return findNodeFromTail(node);
}
// 從尾結點向前遍歷,找到node節點
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

2.2.1.5 checkInterruptWhileWaiting

前面在分析 await 方法時,執行緒會被阻塞。而通過 signal被喚醒之後又繼續回到上次執行的邏輯中。

檢查await被喚醒時,執行緒有沒有被Interrupt。

如果當前執行緒被中斷,則呼叫transferAfterCancelledWait 方法判斷後續的處理應該是丟擲 InterruptedException 還是重新中斷。

  • 先singal,再interrupt再,重新中斷
  • 先interrupt,再singal,直接丟擲異常
private int checkInterruptWhileWaiting(Node node) {
    // 執行緒是否中斷,會清除中斷標識位。根據判斷是否需要重新中斷
    return Thread.interrupted() ?
        // 判斷當前結點有沒有加入同步佇列
        // 1、已經加入同步佇列中返回false,重新中斷(中斷在singal之後)
        // 2、沒有加入同步佇列中,返回true,丟擲異常(中斷在singal之前)
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

2.2.1.6 transferAfterCancelledWait

final boolean transferAfterCancelledWait(Node node) {
    // CAS成功,當前結點還未從等待佇列加入同步佇列
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        // 把當前結點從等待佇列移除並加入到同步佇列
        enq(node);
        return true;
    }
    //如果cas失敗,則判斷當前node是否已經在同步佇列上,如果不在,則讓給其他執行緒執行
		//當node被觸發了signal方法時,node就會被加到同步佇列上
  	//迴圈檢測node是否已經成功新增到同步佇列中。如果沒有,則通過yield讓出執行緒的cpu排程
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

2.2.1.7 reportInterruptAfterWait

private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    // 直接丟擲異常
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    // 重新中斷,執行緒自己處理
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

static void selfInterrupt() {
     Thread.currentThread().interrupt();
}

2.3 通知

呼叫Condition的signal()方法,將會喚醒在等待佇列中等待時間最長的節點(首節點firstWaiter) , 在喚醒節點之前, 會將節點移到同步佇列中。

2.3.1 signal

public final void signal() {
    // 如果當前執行緒不是鎖的持有者,直接丟擲異常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
  	// 通知等待佇列中的第一個等待者
    if (first != null)
        doSignal(first);
}

2.3.2 doSignal

本方法的主要功能是:被通知的節點從等待佇列中移除,並把被通知的節點加入同步佇列中。

private void doSignal(Node first) {
    do {
        // 首先將要被通知節點的下一個節點設定為等待佇列的firstWaiter
        // 如果當前被通知節點的下一個節點為空
        if ( (firstWaiter = first.nextWaiter) == null)
          	// 等待佇列的尾節點(lastWaiter)設定為空
            lastWaiter = null;
        // 當前被通知節點的下一個節點設為空,舊的firstWaiter從等待佇列中移除
        first.nextWaiter = null;
    } 
  	// 如果被通知節點沒有進入到同步佇列並且等待佇列還有不為空的節點,則繼續迴圈通知
    while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

2.3.3 transferForSignal

把等待佇列中的節點改變WaitStatus後加入同步佇列

final boolean transferForSignal(Node node) {
    // 嘗試將節點狀態設定為0,如果設定失敗,則說明該節點的狀態已經不是Node.CONDITION,進一步說明該節點在沒有等到通知訊號時,被取消.
    // 直接返回false,通知下一個等待者。回到上面的while迴圈
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 將節點放入到同步佇列中。主要是將節點從等待佇列移入到同步佇列中
    // p為同步佇列的當前加入節點的上個節點
    Node p = enq(node);
    int ws = p.waitStatus;
  	// 上個節點被取消,喚醒剛剛加入aqs佇列的節點
    // 設定前置節點狀態為Node.SIGNAL狀態失敗時,喚醒被通知節點代表的執行緒。
    // 此時不喚醒,只能通過lock.unLock釋放鎖。喚醒當前結點
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

3 總結

執行緒 awaitThread 先通過 lock.lock()方法獲取鎖成功後呼叫 condition.await 方法進入等待佇列,而另一個執行緒 signalThread 通過 lock.lock()方法獲取鎖成功後呼叫了 condition.signal 或者 signalAll 方法,使得執行緒awaitThread能夠有機會移入到同步佇列中,當其他執行緒釋放 lock 後使得執行緒 awaitThread 能夠有機會獲取lock,從而使得執行緒 awaitThread 能夠從 await 方法中退出執行後續操作。如果 awaitThread 獲取 lock 失敗會直接進入到同步佇列。

image-20200923233902398

image-20200923234806591