怎麼理解Condition

轉自:http://www.liuinsect.com/2014/01/27/%E6%80%8E%E4%B9%88%E7%90%86%E8%A7%A3condition/


在java.util.concurrent包中,有兩個很特殊的工具類,Condition和ReentrantLock,使用過的人都知道,ReentrantLock(重入鎖)是jdk的concurrent包提供的一種獨佔鎖的實現。它繼承自Dong Lea的 AbstractQueuedSynchronizer(同步器),確切的說是ReentrantLock的一個內部類繼承了AbstractQueuedSynchronizer,ReentrantLock只不過是代理了該類的一些方法,可能有人會問爲什麼要使用內部類在包裝一層? 我想是安全的關係,因爲AbstractQueuedSynchronizer中有很多方法,還實現了共享鎖,Condition(稍候再細說)等功能,如果直接使ReentrantLock繼承它,則很容易出現AbstractQueuedSynchronizer中的API被無用的情況。

言歸正傳,今天,我們討論下Condition工具類的實現。

ReentrantLock和Condition的使用方式通常是這樣的:

C1

運行後,結果如下:

C2

可以看到,

Condition的執行方式,是當在線程1中調用await方法後,線程1將釋放鎖,並且將自己沉睡,等待喚醒,

線程2獲取到鎖後,開始做事,完畢後,調用Condition的signal方法,喚醒線程1,線程1恢復執行。

以上說明Condition是一個多線程間協調通信的工具類,使得某個,或者某些線程一起等待某個條件(Condition),只有當該條件具備( signal 或者 signalAll方法被帶調用)時 ,這些等待線程纔會被喚醒,從而重新爭奪鎖。

那,它是怎麼實現的呢?

首先還是要明白,reentrantLock.newCondition() 返回的是Condition的一個實現,該類在AbstractQueuedSynchronizer中被實現,叫做newCondition()

C3

它可以訪問AbstractQueuedSynchronizer中的方法和其餘內部類( AbstractQueuedSynchronizer是個抽象類,至於他怎麼能訪問,這裏有個很奇妙的點,後面我專門用demo說明 )

現在,我們一起來看下Condition類的實現,還是從上面的demo入手,

爲了方便書寫,我將AbstractQueuedSynchronizer縮寫爲AQS

當await被調用時,代碼如下:

public final void await() throws InterruptedException {
if (Thread.interrupted())
 throw new InterruptedException();
 Node node = addConditionWaiter(); //將當前線程包裝下後,
                                   //添加到Condition自己維護的一個鏈表中。
int savedState = fullyRelease(node);//釋放當前線程佔有的鎖,從demo中看到,
                                       //調用await前,當前線程是佔有鎖的
 
int interruptMode = 0;
 while (!isOnSyncQueue(node)) {//釋放完畢後,遍歷AQS的隊列,看當前節點是否在隊列中,
                           //不在 說明它還沒有競爭鎖的資格,所以繼續將自己沉睡。
                             //直到它被加入到隊列中,聰明的你可能猜到了,
                            //沒有錯,在singal的時候加入不就可以了?
 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);
 }

回到上面的demo,鎖被釋放後,線程1開始沉睡,這個時候線程因爲線程1沉睡時,會喚醒AQS隊列中的頭結點,所所以線程2會開始競爭鎖,並獲取到,等待3秒後,線程2會調用signal方法,“發出”signal信號,signal方法如下:

public final void signal() {
 if (!isHeldExclusively())
 throw new IllegalMonitorStateException();
 Node first = firstWaiter; //firstWaiter爲condition自己維護的一個鏈表的頭結點,
                          //取出第一個節點後開始喚醒操作
 if (first != null)
 doSignal(first);
 }

說明下,其實Condition內部維護了等待隊列的頭結點和尾節點,該隊列的作用是存放等待signal信號的線程,該線程被封裝爲Node節點後存放於此。

C4

關鍵的就在於此,我們知道AQS自己維護的隊列是當前等待資源的隊列,AQS會在資源被釋放後,依次喚醒隊列中從前到後的所有節點,使他們對應的線程恢復執行。直到隊列爲空。

而Condition自己也維護了一個隊列,該隊列的作用是維護一個等待signal信號的隊列,兩個隊列的作用是不同,事實上,每個線程也僅僅會同時存在以上兩個隊列中的一個,流程是這樣的:

1. 線程1調用reentrantLock.lock時,線程被加入到AQS的等待隊列中。

2. 線程1調用await方法被調用時,該線程從AQS中移除,對應操作是鎖的釋放。

3. 接着馬上被加入到Condition的等待隊列中,以爲着該線程需要signal信號。

4. 線程2,因爲線程1釋放鎖的關係,被喚醒,並判斷可以獲取鎖,於是線程2獲取鎖,並被加入到AQS的等待隊列中。

5.  線程2調用signal方法,這個時候Condition的等待隊列中只有線程1一個節點,於是它被取出來,並被加入到AQS的等待隊列中。  注意,這個時候,線程1 並沒有被喚醒

6. signal方法執行完畢,線程2調用reentrantLock.unLock()方法,釋放鎖。這個時候因爲AQS中只有線程1,於是,AQS釋放鎖後按從頭到尾的順序喚醒線程時,線程1被喚醒,於是線程1回覆執行。

7. 直到釋放所整個過程執行完畢。

可以看到,整個協作過程是靠結點在AQS的等待隊列和Condition的等待隊列中來回移動實現的,Condition作爲一個條件類,很好的自己維護了一個等待信號的隊列,並在適時的時候將結點加入到AQS的等待隊列中來實現的喚醒操作。

看到這裏,signal方法的代碼應該不難理解了。

取出頭結點,然後doSignal

C5

private void doSignal(Node first) {
 do {
 if ( (firstWaiter = first.nextWaiter) == null) //修改頭結點,完成舊頭結點的移出工作
 lastWaiter = null;
 first.nextWaiter = null;
 } while (!transferForSignal(first) &&//將老的頭結點,加入到AQS的等待隊列中
 (first = firstWaiter) != null);
 }
 
final boolean transferForSignal(Node node) {
 /*
 * If cannot change waitStatus, the node has been cancelled.
 */
 if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
 return false;
 
/*
 * Splice onto queue and try to set waitStatus of predecessor to
 * indicate that thread is (probably) waiting. If cancelled or
 * attempt to set waitStatus fails, wake up to resync (in which
 * case the waitStatus can be transiently and harmlessly wrong).
 */
 Node p = enq(node);
 int ws = p.waitStatus;
//如果該結點的狀態爲cancel 或者修改waitStatus失敗,則直接喚醒。
 if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
 LockSupport.unpark(node.thread);
 return true;
 }

可以看到,正常情況 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 這個判斷是不會爲true的,所以,不會在這個時候喚醒該線程。

只有到發送signal信號的線程調用reentrantLock.unlock()後因爲它已經被加到AQS的等待隊列中,所以纔會被喚醒。

總結:

     本文從代碼的角度說明了Condition的實現方式,其中,涉及到了AQS的很多操作,比如AQS的等待隊列實現獨佔鎖功能,不過,這不是本文討論的重點,等有機會再將AQS的實現單獨分享出來。


發佈了92 篇原創文章 · 獲贊 28 · 訪問量 72萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章