從ReentrantLock講AQS的Condition

一、用法

下面我們從ReentrantLock切入,講AQS提供的Conditon一系列方法。

先看用法。

private ReentrantLock lock = new ReentrantLock();

private Condition condition=lock.newConditon();

lock.lock()

try{

操作

condition.await();//這裏調用await()讓線程等待,釋放鎖,同理一樣用signal()方法喚醒線程

}catch(){}

finally{

lock.unlock();

}

 

首先我們知道lock()方法是獲取鎖,unlock()方法是釋放鎖,可是這中途需要進行線程調度,放棄鎖咋辦,所以這裏引入了Condition的方法,進行線程釋放鎖。

 

二、Conditon基本屬性

下面我們看具體方法,首先,我們得先聲明一個Condition對象,這個對象不能直接new Condition(),的調用lock的newCondition方法,看這個方法底層,

實際就是new ConditionObject()對象,因爲該類實現了Condtiion接口,實現了await(),signal()等方法。

我們看看這個ConditionObject類裏面提供的屬性。

 

1.這裏的firstWaiter,lastWaiter是兩個node對象,因爲我們要維護一個條件對列,它是一個單向隊列,所以這裏用這兩個對象維護這個隊列。

2.這裏的REINTERRUPT,THROW_IE是區分線程被中斷的情況,這裏照樣挖個坑,之後解釋(坑1).

 

三、await方法初次分析

3.1 整體流程

下面我們看看condition.await()方法,從上面已知,這裏實際是調用AQS中的一個內部類ConditionObject的await()方法,下面我們去看看這個方法。

我們先梳理下這個方法實現功能的思路,然後再深入每個具體的方法裏面。

1.首先,如果調用該方法的線程被中斷了,那麼不用說,不用往下執行了,直接拋異常就好了。

2.下面進行正式功能,首先通過addConditionWaiter()方法,把調用該方法的當前線程封裝成Node節點並加入條件隊列中。

3.然後我們通過fullyRelease()方法釋放當前線程所獲取的的鎖。

4.之後通過while語句,判斷這個調用await的線程所在的節點是不是在AQS的雙向等待隊列中。(這裏注意區分下,AQS中通過Node中的pred,next維護了一個雙向的等待隊列,這個是lock方法是進行入隊的隊列。同時Node中通過nextWaiter維護了一個單向的條件隊列,這個是await()方法入隊的隊列)。如果節點在等待隊列中,說明已經用signal或者signalAll方法了,所以進入等待隊列中,如果不在等待隊列中,說明還在條件隊列中,那麼調用park方法讓這個線程掛起。

5.如果這個線程被移到了等待隊列中,那麼嘗試通過acquireQueued方法進行進行獲取鎖。

這裏只是講了大致流程。具體細節都沒有分析。下面我們一點點看細節。

3.2 addConditionWaiter方法

首先我們會通過addConditionWaiter()方法,把調用該方法的當前線程封裝成Node節點並加入條件隊列中。我們來看看這個方法是咋做的?

首先我們判斷尾節點lastWaiter是不是null或者是被取消狀態的節點,如果是的話,我們調用unlinkCancelledWaiters()方法清除條件隊列中狀態爲取消狀態的節點。然後讓t等於新的lastWaiter節點。

然後我們把當前線程封裝成一個node節點,類型是CONDTION,表明這個node節點是在條件隊列中的。

之後我們判斷t是不是爲null,如果是,說明條件隊列裏面沒有節點,那麼把這個新生成的節點作爲firstWaiter頭結點。(這裏注意區分下,firstWaiter就是真正有意義的頭結點,而AQS中的head是個傀儡節點,它的下一個節點纔是有意義的頭結點)如果t不爲null,說明條件隊列不爲空,那麼讓lastWaiter的下一個節點爲node,然後讓lastWaiter指向node。其中我們會發現我們沒有用任何的CAS操作保證其一致性,因爲調用該方法時線程還獲取這鎖,所以不用考慮多線程競爭的情況。

其中用到了unlinkCancelledWaiters()方法清除條件隊列中狀態爲取消狀態的節點,下面我們看看這個方法。

這裏用trail表示要被清除節點的前一個節點,t表示要被清除的節點。

我們從頭開始遍歷這個條件隊列。如果t的狀態不是CONDITION,那麼就t.nextWaiter=null.切斷它對下一個節點的指向,然後,如果trail==null,說明這個節點是條件隊列的頭,那麼設置firstWaiter爲下一個節點。否則,讓當前節點的前一個節點指向後一個節點,這樣這個當前節點就被刪除了。

3.3.fullyRelease方法

node加完條件隊列後,我們看看如何釋放鎖的。

這裏的node就是剛剛入條件隊列的節點,也是需要釋放鎖的線程節點。

1.首先我們通過getState方法去當前狀態值,因爲只有當狀態值爲0的時候才表示沒有線程持有這個鎖,這裏和unlock方法有所不同,unlock方法每次只是把state值減一,只有減到0纔是完全釋放鎖,而這裏,調用await()方法就需要完全釋放鎖,所以會一次性把state清除到0.

2.我們通過release()方法進行釋放鎖,這裏也不贅述了,上一章也講過了。如果成功,那麼就設置failed爲fasle,表明釋放釋放成功了。如果沒有則拋異常,說明當前調用這個方法的線程不是持有鎖的線程。

這裏有個細節,對於ReentrantLock而言,個人認爲不會走到這一步,因爲release裏面會調用tryRelease方法,這個方法是ReentrantLock重寫的,那裏面就判斷了釋放鎖的線程是不是獲取鎖的線程,如果不是,會拋出異常。

 

3.4 isOnSyncQueue方法

我們來看isOnSyncQueue方法,用來判斷當前節點在不在等待隊列中。

1.如果當前節點的狀態是CONDITION或者沒有pred節點,那麼肯定不在等待隊列中。

2.如果當前節點的next不爲null,說明肯定在等待隊列中。

3.這裏還有其他情況,就是它的狀態已經不是CONDITION了,也通過pred鏈接到了等待隊列中了,但是還沒有通過next鏈接上(上一章分析過這種情況)這個時候用findNodeFromTail方法,從後遍歷,能找到就返回true.

 

鎖釋放後,我們就需要進行線程狀態的掛起了。通過park方法進行線程的掛起,但這裏有個問題,爲啥要用while循環判斷(坑2)?

到這裏我們就先停止分析await()方法,先進行分析signal方法。

 

四、signal方法

4.1 整體流程

首先通過isHeldExclusively方法判斷排他模式下狀態值是否被佔用了,如果被佔用了,那麼拋出異常。這裏Reentrantlock重新了這個方法,

照樣也是判斷調用這個方法的線程是不是獲取鎖的線程。不是就拋異常。

如果是,那麼先去條件隊列頭結點,如果是null,說明條件隊列中沒有值,那麼就不存在釋放了。

如果有值,我們調用doSignal方法釋放條件隊列的頭結點。

4.2 doSignal方法

這裏使用do{}while()方式,先斷開first節點與後面節點的連接,相當於把這個節點在條件隊列中刪除,再通過while方法中的transferForSignal方法,把節點移到等待隊列中。

下面我們看下transferForSignal方法做了啥?

1.首先,我們需要把node節點從條件隊列移到等待隊列中,它原本的狀態是CONDITION,而進入等待隊列,相當於重新入隊,它的初始狀態是0,所以我們需要把的狀態值從CONDITION改爲0.如果這裏更改失敗了,說明node節點的狀態不是CONDITION了,在條件隊列中的節點,狀態除了CONDITION,只剩CANCELLED,說明該節點已經被取消了,那麼返回false,!transferForSignal就是true,然後就需要把下一個條件隊列節點進行出隊操作,所以這裏加了一個判斷下一個條件節點是不是爲null的判斷(first = firstWaiter) != null,如果不爲null,再次進行循環,進行節點刪除。

2.然後我們就需要把這個出了條件隊列的節點進行入隊到等待隊列中。看到這裏可以理解爲啥要把節點狀態設置爲0了,在lock方法中,節點入等待隊列時節點的狀態都是0,表示默認值,所以這塊就相當於一個新節點入等待隊列了。之後通過enq方法把該節點進行入隊。(這裏有個小疑問,在lock中,入隊用的是addWaiter()方法,先嚐試入隊一次,不成功再用enq入隊,這裏爲啥不用addWaiter,直接用enq方法?)到這裏,signal操作其實就差不多了,這個方法是不負責喚醒這個節點線程的,只有在某些特殊情況下才會喚醒。

 

這個時候我們回憶下調用lock方法的時候是怎麼入隊的,我們先通過addWaiter方法,入隊列,此時入隊node狀態爲0.然後會調用acquireQueued()方法,這裏會判斷這個節點的前一個節點是不是head節點,如果是並能獲取到鎖,那麼萬事ok,如果不行,這裏會調用一個shouldParkAfterFailedAcquire方法來把自己前一個沒有被取消的節點的狀態設置爲SIGNAL。假設一種場景,一個節點node1入隊了等待隊列,它的狀態會是0,等待下一個節點node2入隊列時,正常情況下,會通過這個方法把node1節點的狀態從0設置爲SIGNAL。設置完後就會park(掛起)node2。(這裏可參考上篇文章)

同理,我們回到這個問題,我們來看這個if語句,這裏入隊列的node節點是被掛起的,如果它的前一個節點的狀態不是取消狀態,那麼我直接更改它的狀態成SIGNAL,就好了,說明我這個節點是需要被喚醒的,省掉了喚醒它,再調用acquireQueued方法,然後再通過shouldParkAfterFailedAcquire設置前一個節點狀態爲SIGNAL,然後再掛起這個過程。當然,如果你的前一個節點的狀態是取消狀態或者更改狀態到SIGNAL失敗,那麼就需要喚醒重新調用acquireQueued方法,然後通過shouldParkAfterFailedAcquire方法刪除前面狀態爲取消狀態的節點。

這裏有個問題,transferForSignal 方法在doSignal中調用的,而對於signal方法調用而言,在ReentrantLock中必須獲得鎖,才能調用這個,否則拋異常。那麼既然獲取到了鎖,爲啥這裏還通過CAS操作來保持一致性。這裏可以看下,

它是通過isHeldExclusively方法來判斷是否拋異常,這個方法是可以重寫的,對於ReentrantLock方法而言,不是獲取到鎖的線程調用這個方法就會拋異常,但是如果我們基於AQS封裝其他的鎖,這裏的判斷條件就不一定是一定要獲取到鎖才能調用這個方法了,所以存在多線程競爭的可能,所以通過CAS操作進行控制。

 

到這裏signal方法就講完了 ,這裏順帶講講signalAll方法。

這裏前面都是一樣的,區別在doSignalAll()方法不一樣。我們看看這個方法。

這個方法和doSignal對比,就兩個地方不一樣,一個是把lastWaiter和firstWaiter都設置爲null,因爲該方法是把條件隊列的節點都出隊,所以設置爲null,二是把transferForSignal方法移到循環體內了,因爲該方法就是把節點添加到等待隊列中,所以需要把出隊的都加進去,而在doSignal中只要加一個節點就好。

五、await方法再次分析

下面我們接着迴歸await方法,這個方法只是講了主體,還有挺多有意思的沒講。

這裏開始填坑2,因爲對於await()方法而言,調用它後這個線程節點就應該在條件隊列中了,如果在等待隊列的話,就說明被signal過了,移到了等待隊列了,這樣就可以用等待等待隊列的acquireQueued方法去對待它了,不用再因爲是條件隊列而去掛起它。但是這裏爲啥要用while語句呢?我們可以看到,當它被signal方法調用後喚醒,就會被移動等待隊列中,那麼直接不滿足這個判斷條件,就可以退出了,如果是被中斷喚醒的,那麼通過break,也可以退出while。個人的看法可能是節點在等待隊列中時,其他線程不是通過signal或signalAll方法移動這個節點,而是直接unpark這個節點,那麼這裏的while就有意義了。(歡迎其他不同想法)

之前講到線程被加到條件隊列中,然後被park掛起了。正常情況下有兩種情況被喚醒,一個就是signal方法(上面說的),另一個就是在等待隊列中被unlock方法喚醒。

除了這兩種,它同時也會被中斷喚醒,這裏我們討論的就是這種異常情況。

首先我們通過checkInterruptWhileWaiting方法判斷這個線程是被中斷喚醒的,還是正常喚醒的,如果是正常的,那麼返回值爲0。如果是中斷喚醒的,那麼這個根據被中斷的時機不同返回不同的值。

下面看transferAfterCancelledWait方法。

通過這個方法來決定中斷類型。

首先這裏設置兩個變量,表明不同的中斷類型,其中REINTERRUPT表示這設置中斷狀態,不對外拋中斷異常。THROW_IE表示對外拋中斷異常。(開始填坑1)

我們根據transferAfterCancelledWait方法來決定哪種方式暴露對外中斷類型。

這裏我們從線程被await()方法休眠到被signal方法加到等待隊列的過程中,它處的不同狀態時候都有可能被中斷。總共有四種情況。

1.在條件隊列中的時候被中斷。也就是再被調用doSignal之前。這是節點的狀態爲CONDITION

2.在doSignal方法中,先在條件隊列中刪除這個節點,然後再調用transferForSignal方法,在這個方法的

前被中斷。這個時候節點不在條件隊列中,也不在等待隊列中。狀態爲CONDITION

3.節點被從條件隊列刪除了,狀態也被更改爲0了,但是還沒有入隊到等待隊列中。也就是還沒執行enq方法。被中斷

4.節點在等待隊列中。被中斷。

下面通過這個四種情況再結合transferAfterCancelledWait方法看。

1.前面有個if語句,判斷能不能把節點狀態從CONDTION改成0。啥時候節點狀態是CONDITON,就是我上述說的情況1,2。在這種情況下,我們把它入隊到等待隊列中,並返回true,也就是THROW_IE.在這裏對於情況1,如果被入隊到等待隊列中,因爲它還沒有從條件隊列中刪除,所以這是這個節點即在條件隊列中,也在等待隊列中。

2.如果狀態不是CONDITION,說明是情況3,4,這裏用到while語句,就是用來判斷情況3的,如果沒有入隊,那就線程等待會,讓它入隊。這個時候返回的就是false,也就是REINTERRUPT。

迴歸到await方法。checkInterruptWhileWaiting我們通過線程是否是被中斷喚醒的,來判斷來決定是否跳槽循環。如果是被中斷的,那就跳出循環往下執行。在這裏的時候被喚醒的節點不管是被正常喚醒還是被中斷喚醒,都在等待隊列中了。然後我們通過acquireQueued方法在等待隊列中排隊獲取線程,如果在等待隊列中獲取鎖了,那就往下執行,如果沒有,那麼在等待隊列中會接着被掛起。然後到interruptMode != THROW_IE方法,執行到這說明兩個問題,一個是這個之前被await的線程已經再次獲取到鎖了,二是acquireQueued返回true,也就是說在等待隊列過程中被中斷喚醒過,也就是對應着情況4,這個時候設置中斷類型爲REINTERRUPT。

往下看,這個有個這個方法,

在正常情況下,條件隊列的節點都是現在條件隊列中刪除,然後入隊到等待隊列中,但是在一種情況下例外,就是我說的情況1,所以這個時候調用下這個方法判斷下,如果成立,那麼得把這個節點從條件隊列刪除。

然後看這個方法

 

這個方法就是根據中斷類型的不同覺得不同的處理方式,如果是THROW_IE,就拋異常,如果是REINTERRUPT,通過

方法設置中斷狀態爲中斷過。其實總結起來就是如果是在條件隊列中被中斷了,最後會拋出一箇中斷異常,如果是在等待隊列中被中斷了,最後會設置線程中斷狀態位爲已被中斷。

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