【Java併發編程】AQS(5)——ConditionObject

這篇文章是AQS系列的最後一篇文章,也是非常重要的一篇,因爲這篇文章將引入併發編程中非常重要的一個概念:條件變量。在聊條件變量之前我想先聊聊管程(monitor),下面是對管程的描述:

在併發程序中,管程是一種同步結構,它不僅允許線程擁有互斥和等待條件變化的能力,其還可以告訴其他線程條件是否滿足。管程是由一個互斥量和多個條件變量構成一個條件變量實質上是一個等待條件的容器。在再次獲得互斥量執行任務之前,管程給線程提供了暫時放棄互斥量有序等待條件滿足的機制

AQS其實就是對管程這個模型的具體實現,我們回想AQS系列的前四篇文章,就是對互斥量的實現,而今天,介紹的就是條件變量。每個條件變量都會有兩個方法,喚醒和等待。當條件滿足時,我們就會通過喚醒方法將條件容器內的線程放入同步隊列中;如果不滿足條件,我們就會通過等待方法將線程阻塞然後放入條件隊列中。下圖是管程的模型圖,正確的來說是Mesa管程的模型圖,管程一共有三種類型, AQS使用的是Mesa(PS:原圖是從維基上copy的,自己改了下,這裏申明下)

                                                                  

我們可以看到,裏面很多的小圓圈就是我們前面說的包裝了線程的Node,線程只有進入到臨界區,即拿到互斥量(鎖)了以後,才能夠調用喚醒方法notify和等待方法wait。管程,互斥體,信號量這些都是操作系統裏面的知識,這些雖然不難,但卻很重要,今天主要是講AQS中與條件變量相關的ConditionObject類,所以就不過多的擴展了,這東西真要講可能就是一篇單獨的文章了,如果大家感興趣,可以自己去網上找找相關資料看下

 

 

一.  屬性

 

我們先看下ConditionObject中的屬性

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

前面說了,每個條件變量都維護了一個容器,ConditionObject中的容器就是單向鏈表隊列,上面的屬性就是隊列的頭結點firstWaiter和尾結點lastWaiter,需要注意,條件隊列中的頭結點不是虛擬頭結點,而是包裝了等待線程的節點!其類型和同步隊列一樣,也是使用AQS的內部類Node來構成,但與同步隊列不同的是,條件隊列是一個單向鏈表,所以他並沒有使用Node類中的next屬性來關聯後繼Node,而使用的nextWaiter

volatile Node prev;
volatile Node next;
Node nextWaiter;

這裏我們需要注意,nextWaiter是沒用volatile修飾的,爲什麼呢?因爲線程在調用await方法進入條件隊列時,是已經擁有了鎖的,此時是不存在競爭的情況,所以無需通過volatile和cas來保證線程安全。而進入同步隊列的都是搶鎖失敗的,所以肯定是沒有鎖的,故要考慮線程安全

最後需要注意一點的是,條件隊列裏面的Node只會存在CANCELLED和CONDITION的狀態

屬性知道了後,我們來看看方法吧,我們首先介紹喚醒相關的方法

 

 

二.  方法signalAll、signal

 

1   signalAll

顧名思義,就是將條件隊列中的所有Node移到同步隊列中,然後根據條件再喚醒它們去嘗試獲得鎖

public final void signalAll() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignalAll(first);
}

首先我們會通過我們子類複寫的方法isHeldExclusively來看此時的線程是否已經獲得了鎖。前面說過只有獲得了鎖的線程才能夠去喚醒條件隊列中的Node。如果獲得了鎖,我們會判斷條件隊列的頭結點是否爲null,爲null則說明條件隊列中沒有阻塞的Node;如果不爲null,則會通過doSignalAll方法來將條件隊列中的所以Node移動到同步隊列中

 

1.1  doSignalAll

private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
       // 將next指向first的後繼Node
        Node next = first.nextWaiter;
       // 切斷first與後繼Node的聯繫
        first.nextWaiter = null;
       // 將此node轉移到同步隊列中
        transferForSignal(first);
        // 將first指向first的後繼Node
        first = next;
    // 在判斷此時的first是否爲null,不是則繼續循環
    } while (first != null);
}

因爲是移出條件隊列中所有的Node,所以一開始我們通過將頭結點和尾節點置爲null來“清空”條件隊列,然後通過do-while循環將條件隊列中所有節點通過transferForSignal方法一個一個轉移到同步隊列中

 

1.2  transferForSignal

final boolean transferForSignal(Node node) {
    // 說明此節點狀態爲CANCELLED,所以跳過該節點(GC會回收)
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;
    // 入隊方法(獨佔鎖獲取中詳細闡述過)
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread); 
    return true;
}

我們首先會通過CAS操作來將Node的狀態置爲0,如果失敗了,說明此時Node狀態是CANCELLED,則我們跳過,返回false;如果Node狀態成功置爲了0,我們就通過enq方法進行入隊,此方法已在"獨佔鎖的獲取"詳細說過,但這裏還是需要提醒一下,enq返回的是Node的前驅節點。然後我們會根據前驅節點的狀態來看此時是否要喚醒此節點,如果是下面這兩種情況,則會將其喚醒,去嘗試獲取鎖

  • 如果前驅節點狀態是CANCELLED

  • 前驅節點不是CANCELLED狀態,但CAS將狀態變爲SIGNAL失敗

如果將前驅節點賦值SIGNAL成功了,則該節點就需要等到前驅節點釋放鎖之後被喚醒了,我們需要注意,只要節點狀態不是CANCELLED,transferForSignal方法最後都是返回true

 

2  signal

signalAll是將條件隊列中所有的Node轉移到同步隊列,signal則只轉移條件隊列中的第一個狀態不爲CANNCELLED的Node,直接看源碼

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

和signalAll基本一樣,不同點只在doSignal方法,我們接着來看doSignal

private void doSignal(Node first) {
    do {
        // 將firstWaiter指向傳入的first的後繼節點,
        // 然後判斷firstWaiter是否爲null,
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

我們可以看到方法裏面是個do-While的循環,我們首先將firstWaiter指向first的後繼節點,然後判斷first的後繼節點是否爲空,如果爲空,則說明條件隊列中只有first這一個節點,所以我們將整個隊列清空,即將lastWaiter = null。然後我們再將first的的nextWaiter指向null,然後進入while條件語句中。

while條件語句中,首先調用transferForSignal,如果返回爲false,說明節點進入同步隊列失敗(已經被取消了),則我們會判斷此節點的下一個節點是否爲null,即(first = firstWaiter) != null) ,如果不爲null,則會再次進入循環將這個節點進行入隊,否則就不會進入到循環隊列了;當然,如果transferForSignal返回true,則說明此節點入隊成功了,則我們就會退出循環了

 

 

三.  方法wait—阻塞前

 

喚醒方法wait,我們分兩小節講,這一小節我們講解wait方法線程阻塞前的代碼,下一小節我們講wait被喚醒後的代碼

 

1  await

與喚醒方法相反,wait就是將節點入隊並阻塞,等到其他線程喚醒(signal)或者自身中斷後再重新去獲取鎖

public final void await() throws InterruptedException {
    // 如果此線程被中斷過,直接拋中斷異常
    if (Thread.interrupted())
        throw new InterruptedException();
    // 將當前線程包裝成節點放入條件隊列
    Node node = addConditionWaiter();
    // 釋放當前線程持有的額鎖
    long savedState = fullyRelease(node);
    // 初始化中斷模式參數
    int interruptMode = 0;
    // 檢查節點s會否在同步隊列中
    while (!isOnSyncQueue(node)) {
       // 不在同步隊列中則阻塞此線程
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 被喚醒後再去獲取鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 當線程是被中斷喚醒時,node和後繼節點是沒有斷開的
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 根據異常標誌位對異常進行處理
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);

 

1.1  addConditionWaiter

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

首先我們會把節點t指向lastWaiter,如果lastWaiter不是null且它的等待狀態不是CONDITION,說明lastWaiter的狀態是CANCELLED,所以我們會通過unlinkCancelledWaiters方法來移除條件隊列中所有CANCELLED的節點,然後將t指向新的lasterWaiter,所以我們可以看到,只要尾結點是CANCELLED,就會將條件隊列的所有CANCELLED節點移除

然後我們會將當前線程包裝成一個節點,但是與同步隊列初始化節點時不同,條件隊列新建節點時會把狀態置爲CONDITION,而同步隊列則是默認值0,所以條件隊列中的節點只有CONDITION和CANCELLED兩種狀態。然後我們再會判斷下尾結點是否爲null,爲null說明條件隊列爲空,所以我們就將firstWaiter指向新的節點;如果不爲null,就將尾結點的後繼節點指向新節點,然後再重置lastWaiter。最後將新節點返回

addConditionWaiter方法的邏輯大概清楚了,我們再具體看下unlinkCancelledWaiters方法

 

1.1.1  unlinkCancelledWaiters

private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        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;
    }
}

這個就是從頭結點往後遍歷,將Node狀態爲不爲CONDITION的節點移除隊列。這個其實是leetCode中的一個簡單題,但這裏還是講下,我們維護兩個指針t和trail,t指向我們當前需要檢查的節點,而trail指向當前節點的前驅節點,如果當前節點需要移除隊列,則將trail的後繼節點指向當前節點的後繼節點

 

1.2  fullyRelease

我們回到await方法,此時入隊成功後,我們就會調用fullyRelease方法來釋放當前線程所持有的鎖了,我們具體看下源碼

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

其中釋放鎖成功調用的是release方法,這個方法在"獨佔鎖的釋放"中詳述過,需要注意,release除了釋放線程的鎖外,還會將同步隊列中的第一個狀態不爲CANCELLED的節點中的線程喚醒。最終如果釋放鎖成功,我們就會將failed狀態置爲false,然後返回savedState狀態,否則我們就會拋出異常

我們最後看下finally,如果釋放鎖失敗,我們此線程會拋異常終止,那我們這個線程所在的節點狀態就被置爲CANCELLED,然後等待後面被移出條件隊列,所以這也是我們在addConditonWaiter方法中爲什麼要檢查尾結點是否爲CANCELLED的原因

還需要注意的一點是release的入參savedState,這個是獲取重入鎖的數量,不管之前獲得過多少次鎖,release方法都會一起釋放掉,這也是爲什麼這個方法起名爲fullyRelease的原因

 

1.3  isOnSyncQueue

這個方法是查看此節點是否在同步隊列中

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        return true;        
    return findNodeFromTail(node);
}

先看第一個if語句,如果狀態是CONDITION或者prev參數是null,說明此節點是在條件隊列中,返回爲false。我們知道,prev和next都是同步隊列中使用的,所以如果兩個屬性不爲null,說明此節點是在同步隊列中,因此第二個if條件成立則需要返回true。如果兩個if都不成立,說明這個節點狀態是0且prev不爲null,即我們在"獨佔鎖獲取"中CAS進入同步隊列的情況,則我們會通過findNodeFromTail方法來確認是不是這種情況

 

1.3.1  findNodeFromTail

private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

如果此時tail就是node的話,說明node在同步隊列中,如果不是就像前遍歷,但是這裏大家可能有疑問,這個方法沒有考慮到CAS失敗的情況,所以可能存在遍歷不到的情況,我們看下作者對這個方法的註釋

/*
 * 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.
 */
return findNodeFromTail(node);

上面說CAS失敗的情況一般不太可能出現,所以這裏就沒考慮到這種情況了,而且就算沒遍歷到,外層還有一個while自旋呢

我們再回到await方法

// 省略
while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}
// 省略

如果不在同步隊列中,則此線程就被park方法阻塞了,只有當線程被喚醒纔會在這裏開始繼續執行下面代碼

 

 

四.  方法wait—喚醒後

 

public final void await() throws InterruptedException {
    // 省略。。。。
    while (!isOnSyncQueue(node)) {
       // 不在同步隊列中則阻塞此線程
        LockSupport.park(this); // <----- 被喚醒後從下面開始
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 被喚醒後再去獲取鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 當線程是被中斷喚醒時,node和後繼節點是沒有斷開的
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 根據異常標誌位對異常進行處理
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

我們需要注意的是,線程在這裏被喚醒有兩種情況,一種是其他線程調用了doSignal或doSignalAll,還有一種就是線程被中斷(這兩種最終都是調用了unpark方法)。因爲在java中,線程被中斷後並不是馬上就去執行unpark操作,而是先將線程標誌位置爲true告訴操作系統os我需要被中斷,至於os什麼時候來執行中斷,我們也不清楚,所以在這裏,我們需要判斷我們被喚醒的原因到底是因爲中斷還是別的線程喚醒的。這裏我們通過checkInterruptWhileWaiting方法來判斷,但在講這個方法前,我們需要先了解這個interruptMode有幾種狀態

/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT =  1;
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE    = -1;

除了上面兩種,還有一種初始態0,它代表線程沒有被中斷過,不做任何處理。REINTERRUPT代表wait方法退出時,會重新再中斷一次;而THROW_IE則代表wait方法退出時,會拋出InterruptedException異常。瞭解了狀態後,我們來看方法

 

1  checkInterruptWhileWaiting

/**
 * Checks for interrupt, returning THROW_IE if interrupted
 * before signalled, REINTERRUPT if after signalled, or
 * 0 if not interrupted.
 */
private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

我們先看到註釋,如果中斷先於其他線程調用signal等方法喚醒的,則應該返回THROW_IE,而中斷是後於其他線程調用signal等方法喚醒,則返回REINTERRUPT。

我們看下代碼,代碼就是一個嵌套的三元運算符,首先我們會檢查中斷標誌位,如果interrupted方法返回false,說明沒發生中斷,則返回0如果返回了true,則說明中斷了,則我們需要通過transferAfterCancelledWait方法進一步檢查是否發生了其他線程執行了喚醒操作

 

1.1  transferAfterCancelledWait 

final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { 
        enq(node);
        return true;
    }

    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

我們先看第一個if條件,如果條件中的CAS操作成功,說明此時的Node肯定是在條件隊列中,則我們調動 enq 方法將此節點放入到同步隊列中,然後返回true,但是這裏需要特別注意,這個節點的nextWaiter還沒置爲null

如果CAS失敗了,說明這個節點可能已經在同步隊列中或者在入隊的過程中,所以我們通過while循環等待此節點入隊後返回false

我們再回到調用transferAfterCancelled的checkInterruptWhileWaiting方法中,根據transferAfterCancelledWait方法返回值我們最終會返回REINTERRUPT或THROW_IE。

然後我們返回到調用checkInterruptWhileWaiting方法的await方法中

public final void await() throws InterruptedException {
// 代碼省略
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this); 
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // 我們現在在這裏!!!
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) 
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

我們可以看到,如果返回值不爲0,則直接break跳出循環,如果爲0,則再次回到while條件是否檢查是否在同步隊列中。我們繼續往下走看最後三個if語句

我們首先看第一個if語句,我們首先會通過acquireQueued方法來獲取鎖,這個方法在獨佔鎖中詳細講過,但還是再貼出來稍微提一下

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);
    }
}

這個方法首先會檢查下節點是否在同步隊列第一個,如果在,則會再次嘗試獲取鎖然後成功後則會返回true,如果不在同步隊列第一個或者獲取鎖失敗了,則會去掛起,然後等待前驅結點釋放鎖後再被喚醒。如果在剛剛這個過程中,線程又被中斷了,則interrupted則會置爲true,然後最終方法返回爲true(這裏大家沒看懂說明獨佔鎖一節沒理解,建議回看理解了再看這裏)。我們再回到await方法處看if條件

if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
     interruptMode = REINTERRUPT;

如果在獲取鎖的的過程中被中斷了,即acquireQueued返回true,我們再將interruptMode爲0置爲REINTERRUPT。其實簡單來說,第一個if語句就是讓節點去獲取鎖,並且如果在獲取鎖的過程中被中斷了,且此線程之前沒被中斷過,則將interruptMode置爲REINTERRUPT。

我們再來看第二個if語句

if (node.nextWaiter != null) 
    unlinkCancelledWaiters();

啥時候node的nextWaiter不是null。還記得開始說的transferAfterCancelledWait方法嗎,當線程是被中斷喚醒時,node和後繼節點是沒有斷開的,這一步我們的節點中的線程已經獲取鎖了且從同步隊列中移除了,所以我們在這裏將此節點也移除條件隊列,unlinkCancelledWaiters方法前面說過,它會將條件隊列中所有不爲CONDITION的的節點移除

好了到最後一個if語句了,到這裏,線程也拿到鎖了,包裝線程的節點也沒在同步隊列和條件隊列中了,所以wait方法其實已經完成了,所以現在需要對中斷進行善後處理了

if (interruptMode != 0)
    reportInterruptAfterWait(interruptMode);

如果interruptMode不爲0,說明線程是被中斷過的,所以需要對中斷進行處理,我們看下處理方法reportInterruptAfterWait

 

1.2  reportInterruptAfterWait

private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

可以看到,很簡單哈,如果是THROW_IE,就是拋異常,如果是REINTERRUPT,就再自我中斷一次,和獲取獨佔鎖裏面原因一致

好了,wait方法就說完了,喚醒方法除了wait還有幾個,這裏就不再一一講解了,大家如果認真看完wait方法,其他幾個方法應該是非常容易理解的

到這裏,AQS系列的文章就寫完了,如果有朋友對文章有什麼問題或者發現了什麼錯誤,歡迎大家告訴我:)

(完)

 

歡迎大家關注我的公衆號 “程序員進階之路”,裏面記錄了一個非科班程序員的成長之路

                                                         

 

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