6.類似Object監視器方法的Condition接口


  在《1.有關線程、並發的基本概念》中,我們利用synchronized關鍵字、Queue隊列、以及Object監視器方法實現了生產者消費者,介紹了有關線程的一些基本概念。Object類提供的wait的方法和notifyAll方法,與之對應的是Condition接口提供是await和signalAll。await(或wait)是讓當前線程進入等待狀態並釋放鎖,signalAll(或notifyAll)則是喚醒等待中的線程,使得等待中的線程有競爭鎖的資格,注意只是資格,並不代表被喚醒的線程就一定會獲得鎖。

  Condition接口的具體實現還是在AbstractQueuedSynchronizer中的內部實現的——AbstractQueuedSynchronizer$ConditionObject。ConditionObject中維護了一個“等待隊列”,注意這個和AQS同步器維護的“同步隊列”不同。AQS所維護的同步隊列是當前等待資源(同步狀態)的隊列,當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成一個節點並加入到同步隊列中,同時阻塞當前線程,當同步狀態被所持有的線程釋放時會將同步隊列中的首節點喚醒重新獲取同步狀態。而每個Condition維護一個等待隊列,該隊列的作用是一個等待signal信號的隊列。這兩者之間的關系是一個協同的關系,用下圖的說明它們之間的協同過程:

  1. AQS的同步隊列如下圖所示,一個頭結點head指向隊首,一個tail指向隊尾,當線程調用lock()方法獲取鎖而未成功時,線程被構造成節點加入到隊尾。(圖中NodeA是同步隊列的第一個節點,也就是獲得同步狀態的節點)

  2.NodeA調用await()方法時,NodeA從AQS同步隊列中移除,自然也就釋放了鎖,NodeA此時被加入到Condition的等待隊列中,等待signal信號,如下圖所示。

  

  3.執行完第2步后,此時NodeB在同步隊列中處於第一個節點位置,即獲取到了鎖,如果NodeB此時執行signal(或者signalAll)方法,NodeA將會從Condition等待隊列中被移除即被喚醒,加入到同步隊列中,此時NodeA僅僅是被喚醒有了在同步隊列中爭奪資源的資格,並不代表被喚醒后就立即獲得鎖,如下圖所示。

  4. 最后NodeB在signal執行完畢后,調用unLock方法釋放鎖,此時NodeA處於隊首,並爭奪同步狀態。

  以上是AQS的“同步隊列”和Condition的“等待隊列”之間相互協作的過程,下面從源碼解析Condition的主要方法await、signal、signalAll。

 1 public final void await() throws InterruptedException{
 2     if (Thread.interrupted())    //線程被中斷則拋出中斷異常
 3         throw new InterruptedException();
 4     Node node = addConditionWaiter();    //將線程構造為Node節點
 5     long savedState = fullyRelease(node);    //釋放鎖,返回同步狀態
 6     int interruptMode = 0;
 7     while (!isOnSyncQueue(node)) {    //循環判斷當前節點是否在同步隊列中
 8         LockSupport.park(this);
 9         if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
10             break;    //檢查節點在處於等待狀態時是否被中斷
11   }
12   //在跳出了循環,即被signal喚醒后重新加入了同步隊列后,開始重新競爭鎖
13   if (acquireQueued(node, savedState) && interruptMode != THROW_IE)    //acquireQueued自旋獲取鎖,具體分析見《2.從AbstractQueuedSynchronizer(AQS)說起(1)——獨占模式的鎖獲取與釋放》中對獲取同步狀態的解析
14     interruptMode = REINTERRUPT;    
15   if (node.nextWaiter != null) 
16       unlinkCancelledWaiters();    //如果節點從等待狀態轉換為在同步隊列中,並且也已經獲得了鎖,此時將斷開此節點后面的等待節點
17   if (interruptMode != 0)
18       reportInterruptAfterWait(interruptMode);
19  }

  在獲取鎖的線程調用await時,首先會將線程構造為Node節點並釋放鎖,此時線程被移出同步隊列加入到Condition等待隊列中,接着在第7行就會while循環判斷節點是否在同步隊列中,當沒有線程調用signal方法的時候顯然線程不在同步隊列,並將一直循環,直到有線程調用signal方法該線程才會被喚醒加入到同步隊列中,此時才會跳出循環。

  signal和signalAll方法的異同在和notify和notifyAll一樣。signal只會喚醒等待隊列中位於隊首的節點使其具有競爭鎖的資格,而signalAll則會喚醒等待隊列中所有節點使所有節點都具有競爭鎖的資格。

1 public final void signal() {
2     if (!isHeldExclusively())    //判斷當前線程是否持有鎖
3         throw new IllegalMonitorStateException();
4     Node first = firstWaiter;
5     if (first != null)
6         doSignal(first);        //喚醒等待隊列中的第一個節點
7 }

  對比signalAll方法,不同點在於第6行是喚醒等待隊列中的所有節點——doSignalAll(first),不再貼出代碼。

private void doSignal(Node first) {
    do {
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
} while (!transferForSignal(first) && (first = firstWaiter) != null)    //transferForSignal方法將處於等待隊列中的節點添加到同步隊列中
}

  至於doSignalAll則是循環調用transferForSignal使得所有節點都被喚醒加入到同步隊列中。

  當節點從等待隊列中加入到同步隊列中時,呼應await中的循環等待節點是否在同步隊列中,await和signal的協同配合也就很清晰明了了。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM