天天看點

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的擷取和釋放

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的擷取和釋放

AbstractQueuedSynchronizer(AQS)源碼解析(二)——資源的擷取和釋放

上期的《全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(一)AQS基礎》中介紹了什麼是AQS,以及AQS的基本結構。有了這些概念做鋪墊之後,我們就可以正式地看看AQS是如何通過<code>state</code>(以下也稱資源)和同步隊列,實作線程之間的同步功能了 那麼線程之間是如何同步呢?其實就是通過資源的擷取和釋放來進行同步。如果擷取到就繼續運作,擷取不到就放入同步隊列阻塞等待,釋放就是交出獲得的資源,并釋放同步隊列中需要被喚醒的線程。對,就是這麼簡單! 本篇我們繼續深入AQS内部,一起來看看線程是怎麼利用AQS來擷取、釋放資源的~

AQS擷取資源是通過各種<code>acquire</code>方法。不同<code>acquire</code>方法之間存在差別,如下:

<code>acquire</code>:以互斥模式擷取資源,忽略中斷

<code>acquireInterruptibly</code>:以互斥模式擷取資源,響應中斷

<code>acquireShared</code>:以共享模式擷取資源,忽略中斷

<code>acquireSharedInterruptibly</code>:以共享模式擷取資源,響應中斷

<code>acquire</code>方法是擷取互斥資源,忽略中斷。如果擷取成功,直接傳回,否則該線程會進入同步隊列阻塞等待。源碼如下:

<code>acquire</code>是一個模闆方法,定義為final方法防止子類重寫。其中的鈎子方法<code>tryAcquire</code>需要子類去實作。

如果<code>tryAcquire</code>傳回true,說明嘗試擷取成功,直接傳回即可。如果<code>tryAcquire</code>傳回false,說明嘗試擷取失敗,會調用<code>addWaiter</code>方法進入等待隊列。該方法的解析見上一篇部落格全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(一)AQS基礎

執行完<code>addWaiter</code>方法後,該線程就處于同步隊列中了(queued),接下來就會調用<code>acquireQueued</code>方法

<code>acquireQueued</code>方法為一個已經位于同步隊列的線程,以互斥模式擷取資源,不響應中斷但是會記錄中斷狀态。源碼如下:

在<code>acquireQueued</code>方法代碼主要都包含在一個<code>for</code>循環中。如果發現<code>node</code>是隊首節點,就會再次嘗試擷取資源。如果此時擷取成功,就直接出隊并傳回,不用阻塞等待,這裡展現了同步隊列先進先出的特點

如果不是隊首節點,或者是再次嘗試擷取資源又雙叒叕失敗了,則調用<code>shouldParkAfterFailedAcquire</code>方法判斷目前線程是否應該被阻塞

<code>shouldParkAfterFailedAcquire</code>方法會檢查目前線程是否應該被阻塞,如果是就傳回true,否則傳回false。其源碼如下:

隻有當<code>node</code>的直接前驅節點等待狀态<code>waitStatus</code>是<code>SIGNAL</code>時,才會認為該線程應該被阻塞。否則還需要回到<code>acquireQueued</code>的<code>for</code>循環中重新檢查,不會立即阻塞

我畫了一張<code>shouldParkAfterFailedAcquire</code>的執行流程圖,如下:

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的擷取和釋放
那麼會不會有一種可能:<code>shouldParkAfterFailedAcquire</code>方法一直傳回false,始終認為該線程不應該阻塞,那麼該線程就會一直占用CPU資源,“忙等” 其實一般來說是不會的,原因見上面示意圖中的紫色文字部分

再回到<code>acquireQueued</code>方法中,如果<code>shouldParkAfterFailedAcquire</code>判斷該線程,并傳回了true,就需要執行<code>parkAndCheckInterrupt</code>将該線程阻塞,源碼如下:

在<code>parkAndCheckInterrupt</code>中借助了工具類<code>LockSuppport</code>将線程阻塞。阻塞過程中如果該線程被設定了中斷狀态,雖然中斷不會導緻阻塞立即被喚醒,但是線程的中斷狀态會被記錄下來,并作為該方法的傳回值

總體來說,<code>acquireQueued</code>方法的執行流程如下圖所示:

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的擷取和釋放

再回到<code>acquire</code>方法中。如果<code>acquire</code>失敗而阻塞等待的過程中被中斷,那麼等它被喚醒并成功獲得資源之後,會立即調用<code>setInterrupt</code>方法設定線程的中斷狀态。<code>setInterrupt</code>的源碼如下:

最後補充一點,<code>acquire</code>方法除了會線上程擷取互斥資源時被調用,也會被條件等待方法<code>await</code>方法調用,具體分析見本系列最後一期部落格全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(三)條件變量

<code>acquireInterruptibly</code>用于擷取互斥資源。顧名思義,這個方法響應中斷,即如果在調用過程中發生了中斷,會抛出中斷異常,中止資源的擷取。其源碼如下:

<code>acquireInterruptibly</code>方法首先會檢查中斷狀态,如果沒有發生中斷,才會繼續向下執行,否則抛出中斷異常

接下來執行鈎子方法<code>tryAcquire</code>,如果擷取成功則直接傳回,否則擷取失敗,執行<code>doAcquireInterruptibly</code>方法:

<code>doAcquireInterruptibly</code>會先調用<code>addWaiter</code>方法,将目前線程加入隊尾。之後的邏輯和<code>acquireQueued</code>類似,就是在<code>for</code>循環中,先判斷目前節點是否是頭節點,如果是則再次嘗試擷取資源。如果不是隊首或者擷取失敗,則調用<code>shouldParkAfterFailedAcquire</code>方法判斷該線程是否應該被阻塞。如果不是就進入下一輪循環。如果需要被阻塞,則調用<code>parkAndCheckInterrupt</code>方法将其阻塞。如果阻塞過程中發生中斷,則當該線程被喚醒後回到<code>doAcquireInterruptibly</code>中,會抛出中斷異常,并調用<code>cancelAcquire</code>執行取消節點的邏輯

<code>doAcquireInterruptibly</code>和<code>acquireQueued</code>的差別有兩點:

<code>acquireQueued</code>調用之前,目前線程就已經被放入同步隊列;而<code>doAcquireInterruptibly</code>沒有,需要自己調用<code>addWaiter</code>方法

<code>acquireQueued</code>中不會因發生中斷而抛出中斷異常、取消節點,隻會記錄是否發生中斷并傳回;而<code>doAcquireInterruptibly</code>會響應中斷,抛出中斷異常,并取消該線程對應的節點

作者:酒冽        出處:https://www.cnblogs.com/frankiedyz/p/15674098.html

版權:本文版權歸作者和部落格園共有

轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接配接;否則必究法律責任

<code>acquireShared</code>是以共享模式擷取資源,并且忽略中斷。源碼如下:

該方法首先會調用鈎子方法<code>tryAcquireShared</code>嘗試擷取共享資源,如果擷取成功則直接傳回,否則擷取失敗,調用<code>doAcquireShared</code>方法:

這裡也會調用<code>addWaiter</code>将目前線程加入同步隊列,不過這裡的<code>Node</code>是共享模式(<code>Node.SHARED</code>)

在接下來的<code>for</code>循環中,如果目前線程位于隊首,則再次嘗試擷取資源。如果擷取成功,則調用<code>setHeadAndPropagate</code>方法,進行中斷之後傳回

其中<code>setHeadAndPropagate</code>方法的作用是彈出隊頭,并檢測其後繼節點是否需要被喚醒,如果需要的話就喚醒,并確定傳播。源碼如下;

在共享模式下,一個線程擷取資源成功後,可能會引起後繼等待擷取共享資源的線程。注意,這裡是後繼而非同步隊列中所有後面的。在這一點上,不同于互斥資源的擷取,共享資源的擷取更像是一人得道,雞犬升天

如果在<code>setHeadAndPropagate</code>中發現存在後繼線程需要被釋放,則調用<code>doReleaseShared</code>方法将它釋放,并確定傳播,它也是<code>releaseShared</code>方法的核心,該方法會在後面講解釋放共享資源時給出解析,這裡暫時不分析

確定傳播的含義: 保證被喚醒的線程可以繼續喚醒它的後繼線程。如果每個線程都能確定傳播,那麼所有應該被釋放的後繼線程都能得到釋放(類似于遞歸釋放)

總的來說,<code>acquireShared</code>的流程與<code>acquire</code>基本一緻,最大的差別在于:擷取共享資源成功後,可能需要喚醒後繼的多個線程。而擷取互斥資源成功後,不需要喚醒其他任何線程

<code>acquireSharedInterruptibly</code>方法用于擷取共享資源,但是該方法會響應中斷,即在擷取過程中接收到中斷信号,會抛出中斷異常。其源碼如下:

和<code>acquireInterruptibly</code>一樣,<code>acquireSharedInterruptibly</code>也會先檢查線程的中斷狀态是否已經被設定。如果設定則直接抛出中斷異常

接下來會調用鈎子方法<code>tryAcquireShared</code>嘗試擷取共享資源,擷取成功則直接傳回,擷取失敗就會調用<code>doAcquireSharedInterruptibly</code>方法:

不多解釋,直接上圖吧!下面是<code>doAcquireSharedInterruptibly</code>方法的執行流程圖:

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的擷取和釋放

<code>doAcquireSharedInterruptibly</code>方法和<code>doAcquireShared</code>方法大體上差不多,差別僅在于前者響應中斷并會抛出中斷異常,而後者忽略中斷,隻記錄中斷狀态并傳回

AQS釋放資源是通過各種<code>release</code>方法。不同<code>release</code>之間存在差別,如下:

<code>release</code>:以獨占模式釋放對象

<code>releaseShared</code>:以共享模式釋放對象

這些釋放資源的方法都不存在響應中斷的差別,都是忽略中斷的,因為線程在釋放資源的時候被中斷可能引起意外的錯誤

AQS使用<code>release</code>方法釋放互斥資源,源碼如下:

該方法會先調用鈎子方法<code>tryRelease</code>,如果釋放失敗則直接傳回false,如果釋放成功,則調用<code>unparkSuccessor</code>方法喚醒隊首線程,并傳回true

<code>unparkSuccessor</code>方法是喚醒線程的主要邏輯。源碼如下:

該方法的作用是喚醒<code>node</code>的有效後繼節點。有效指的是跳過那些被cancel的節點。 由于同步隊列是FIFO的,是以<code>node</code>一定是<code>head</code>

<code>releaseShared</code>用于釋放共享資源,源碼如下:

該方法首先調用鈎子方法<code>tryReleaseShared</code>嘗試釋放資源,如果失敗則直接傳回false,如果成功則執行<code>doReleaseShared</code>方法喚醒後繼的其他共享模式線程同時確定傳播,最後傳回true

<code>doReleaseShared</code>方法在前面的<code>acquireShared</code> -&gt; <code>setHeadAndPropagate</code>中出現過,該方法的作用是在共享模式下喚醒後繼線程,并確定傳播。其源碼如下:

AQS的應用就不用我多吹了吧,那些個JUC裡面的大名鼎鼎的可重入鎖、讀寫鎖,底層實作都是基于AQS

如果想要自己使用AQS實作某個并發工具,也很簡單,隻需要繼承AQS,并實作一些特定方法即可~

如果要使用AQS中的互斥資源同步方法,需要手動實作<code>tryAcquire</code>和<code>tryRelease</code>方法

如果要使用AQS中的共享資源同步方法,需要手動實作<code>tryAcquireShared</code>和<code>tryReleaseShared</code>方法

如果要使用AQS中的條件變量,需要實作<code>isHeldExclusively</code>方法

非可重入鎖<code>NonReentrantLock</code>定義了一個内部工具類<code>Sync</code>實作關于鎖的操作,而<code>Sync</code>則繼承了AQS。實作的代碼如下:

點選檢視代碼

最後來做個總結:

AQS針對互斥資源、共享資源的擷取和釋放,提供了不同的方法。而擷取資源的方法也可以分為響應中斷和忽略中斷,釋放資源都是忽略中斷的

AQS正是通過資源 (<code>state</code>)的釋放和擷取,配合同步隊列讓線程排隊等待,以FIFO的方式讓競争資源失敗的線程阻塞、喚醒

這些釋放、擷取方法都是AQS提供給子類去調用的模闆方法,其中的一些關鍵步驟均設計為了鈎子方法,讓子類可以個性化定制

正是有了AQS這個強大的後盾,才能誕生出那麼多實用的并發同步工具類。不得不說,AQS是真的牛啊

好了,能看到這裡的讀者,相信已經掌握了AQS的基本結構,以及AQS是擷取、釋放資源的原理 我這裡其實并沒有剖析所有AQS提供的資源擷取方法,還有兩個可逾時方法<code>tryAcquireNanos</code>、<code>tryAcquireSharedNanos</code>沒有分析,但是基本上和其他擷取資源方法是類似的,隻是多了一個逾時而取消的邏輯,感興趣的讀者可以打開AQS源碼自己分析 接下來的就是AQS的最後一篇了,我們來看看AQS裡面的條件隊列是怎麼實作的

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(一)AQS基礎

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的擷取和釋放

全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(三)條件變量

願歸來仍是少年!

    作者:酒冽

    出處:https://www.cnblogs.com/frankiedyz/p/15674098.html

    版權:本文版權歸作者和部落格園共有

    轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接配接;否則必究法律責任