天天看點

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

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

AbstractQueuedSynchronizer(AQS)源碼解析(一)——AQS基礎

<code>AbstractQueuedSynchronizer</code>(以下簡稱AQS)的内容确實有點多,部落客考慮再三,還是決定把它拆成三期。原因有三,一是放入同一篇部落格勢必影響閱讀體驗,而是為了表達對這個偉大基礎并發元件的崇敬之情。第三點其實是為了偷懶。 又扯這麼多沒用的,還是直接步入正題吧~

AQS是一個抽象類,它是實作多種并發同步工具的核心元件。比如大名鼎鼎的可重入鎖(<code>ReentrantLock</code>),它的底層實作就是借助内部類<code>Sync</code>,而<code>Sync</code>類就是繼承了AQS并實作了AQS定義的若幹鈎子方法。這些并發同步工具包括:

<code>ReentrantLock</code>(ReentrantLock可重入鎖—源碼詳解)

<code>ReentrantReadWriteLock</code>(全網最詳細的ReentrantReadWriteLock源碼剖析(萬字長文))

<code>Semaphore</code>(Semaphore信号量源碼解析)

<code>CountDownLatch</code>(CountDownLatch源碼閱讀)

從設計模式上來看,AQS主要使用的是模闆方法模式(Template Method Pattern)。它提供了若幹鈎子方法供子類實作(如<code>tryAcquire</code>、<code>tryRelease</code>等),AQS的模闆方法(如<code>acquire</code>、<code>release</code>等)會調用這些鈎子方法。子類使用AQS的方式就是直接調用AQS的模闆方法,并重寫這些模闆方法涉及到的特定鈎子方法即可。不需要調用的鈎子方法可以不用重寫,AQS為它們均提供了預設實作:抛出<code>UnsupportedOperationException</code>異常

此外,AQS也提供了其他一些方法供子類調用,如<code>getState</code>、<code>hasQueuedPredecessors</code>等方法,友善子類擷取、判斷同步器的狀态

什麼是鈎子方法? 鈎子方法的概念源于模闆方法模式,這種模式是在一個方法中定義了算法的骨架,某些關鍵步驟會交給子類去實作。模闆方法在不改變算法本身結構的情況下,允許子類自定義其中一些關鍵步驟 這些關鍵步驟可以由父類定義成方法,這些方法可以是抽象方法,或鈎子方法 抽象方法:父類定義但不實作,由<code>abstract</code>關鍵字辨別 鈎子方法:父類定義且實作,但這種實作一般都是空實作,并沒有任何意義,這麼做隻是為了友善子類根據需要重寫特定的鈎子方法,而不用實作所有的鈎子方法

AQS的核心思想:

使用一個<code>volatile int</code>變量<code>state</code>(也被稱為資源),進行同步控制,但是<code>state</code>在不同的同步工具實作中具有不同的語義。另外配合<code>Unsafe</code>類提供的CAS操作,原子性地修改<code>state</code>值,保證其線程安全性

AQS内部維護了一個同步隊列,用來管理排隊的線程。另外需要借助<code>LockSupport</code>類提供的線程阻塞、喚醒方法

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

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

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

AQS使用<code>volatile int</code>變量<code>state</code>來作為核心狀态,所有的同步控制都是圍繞這個<code>state</code>來進行的,<code>volatile</code>保證其記憶體可見性,并使用CAS確定<code>state</code>的修改是原子的。<code>volatile</code>和CAS同時存在,就保證了<code>state</code>的線程安全性

對于不同的同步工具實作來說,語義是不同的,如下:

<code>ReentratntLock</code>:表示目前線程擷取鎖的重入次數,0表示鎖空閑

<code>ReentrantReadWriteLock</code>:<code>state</code>的高16位表示讀鎖數量,低16位表示寫鎖數量

<code>CountDownLatch</code>:表示目前的計數值

<code>Semaphore</code>:表示目前可用信号量的個數

針對<code>state</code>這個核心狀态,AQS提供了<code>getState</code>、<code>setState</code>等多個擷取、修改方法,源碼如下:

AQS内部維護了一個同步隊列(網上有些文章會叫它為CLH隊列,至于為啥叫這個我也不知道-_-||,但不重要~)。隊列中的每個節點都是<code>Node</code>類型其源碼如下:

<code>prev</code>、<code>next</code>用于儲存該節點的前驅、後繼節點,表明這個同步隊列是一個雙向隊列

<code>Node</code>的<code>thread</code>域儲存了對應的線程,隻有在建立時指派,使用完要null掉,以友善GC

<code>Node</code>使用<code>SHARED</code>、<code>EXCLUSIVE</code>兩個常量來标記該線程是由于擷取共享資源、互斥資源失敗,而被阻塞并放入到同步隊列中進行等待

<code>Node</code>使用<code>waitStatus</code>來記錄目前線程的等待狀态,通過CAS進行修改。它的取值可以是:

<code>CANCELLED</code>:表示該節點由于逾時或中斷而被取消。該狀态不會再轉變為其他狀态,而且該節點的線程再也不會被阻塞

<code>SIGNAL</code>:表示其後繼節點(後面相鄰的那個節點)需要被喚醒,即該線程被釋放或被取消時,必須喚醒其後繼節點

<code>CONDITION</code>:表示該節點的線程在條件隊列中等待,而非在同步隊列中。如果該條件變量<code>signal</code>該節點後,該節點會被轉移到同步隊列中參與資源競争

<code>PROPAGATE</code>:隻有在共享模式下才會被用到,表示無條件傳播狀态。引入這個狀态是為了解決共享模式下并發釋放而引起的線程挂起的bug,這裡不多解釋,網上有文章給出了更加詳細的解釋,見下方

AQS:為什麼需要PROPAGATE? AQS源碼深入分析之共享模式-你知道為什麼AQS中要有PROPAGATE這個狀态嗎?

AQS中維護了一個同步隊列,它通過兩個指針标記隊頭、隊尾,分别是<code>head</code>和<code>tail</code>,源碼如下:

該隊列的出入規則遵循FIFO(First In, First Out)

注意:如果該同步隊列非空,那麼<code>head</code>其實并不是指向第一個線程對應的<code>Node</code>,而是指向一個空的<code>Node</code>

接下來讓我們剖析一下AQS針對這個同步隊列設計的入隊、出隊算法

入隊事件主要線上程嘗試擷取資源失敗時觸發。當線程嘗試擷取資源失敗之後,會将該線程加入到同步隊列的隊尾

入隊算法的源碼見AQS的<code>addWaiter</code>方法,如下:

首先為該線程建立一個<code>Node</code>節點,<code>mode</code>可以是<code>Node.EXCLUSIVE</code>或<code>Node.SHARED</code>,表示兩種不同的模式。

之後直接CAS試圖将其入隊。這裡注意,如果隊列本身為空,或CAS競争失敗,才會進入<code>enq</code>方法。這裡<code>addWaiter</code>方法出于性能考慮,先嘗試快捷的入隊方式,不成功才執行<code>enq</code>方法

<code>enq</code>方法是完整的入隊邏輯,源碼如下:

<code>enq</code>中的代碼都包含在<code>for</code>循環中,如果CAS失敗,就會不斷循環CAS直到成功為止

注意,這段代碼也展現出同步隊列的三個特點:

入隊都是從隊尾

進入隊列的操作都是CAS操作,保證了線程安全性

如果隊列為空,則<code>head</code>和<code>tail</code>都為null;如果不為空,<code>head</code>指向的節點并不是第一個線程對應的節點,而是一個啞節點

出隊事件主要發生在:位于同步隊列中的線程再次擷取資源,并成功獲得時

出隊算法在AQS中并沒有直接對應的方法,而是零散分布在某些方法中。因為擷取資源失敗而被阻塞的線程被喚醒後,會重新嘗試擷取資源。如果擷取成功,則會執行出隊邏輯

例如,在<code>acquireQueued</code>方法中,就包含了出隊事件:

出隊的邏輯展現在第6-9行,此時<code>p</code>指向<code>head</code>指向的空節點,而<code>node</code>是隊首元素(不是第一個空節點)

首先調用<code>setHead</code>方法,将<code>head</code>指向<code>node</code>、将<code>node</code>的<code>thread</code>域、<code>prev</code>域置空,然後将<code>head</code>的<code>next</code>域置空,以友善該節點的GC

線程會因為逾時或中斷而被取消,之後不會再參與鎖的競争,會等待GC

取消的過程見<code>cancelAcquire</code>方法,該方法的調用時機都是在擷取資源失敗之後,而失敗就是由于逾時或中斷。其源碼如下:

總之,<code>cancelAcquire</code>方法就是将目标節點<code>node</code>的<code>thread域置空</code>,并将<code>waitStatus</code>置為<code>CANCELLED</code>

這裡有一個問題:<code>node</code>的後繼節點<code>next</code>的<code>prev</code>指針仍然指向<code>node</code>,沒有更新為<code>pred</code>,這不僅語義上是錯誤的,而且會阻礙<code>node</code>被GC。那麼何時進行更新? 答:任何其他線程嘗試擷取鎖失敗之後,都會被放入同步隊列,然後調用<code>shouldParkAfterFailedAcquire</code>方法判斷是否應該被阻塞。如果發現目前節點的前驅節點被置為<code>CANCELLED</code>,就會執行:

此外,<code>cancelAcquire</code>方法也會做類似的操作,如下:

這兩處都會更新被取消節點的後繼節點的<code>prev</code>指針,是以前面說到的的問題根本不存在

注意:<code>cancelAcquire</code>的調用時機一般都是在擷取鎖邏輯後面的<code>finally</code>塊中,如果擷取失敗就會調用<code>cancelAcquire</code>方法。擷取失敗的原因主要有兩個,中斷或逾時

總結:

節點被取消的原因:擷取鎖逾時或在擷取的過程中被中斷

取消節點的主要邏輯:将其<code>waitStatus</code>修改為<code>CANCELLED</code>。再将節點<code>thread</code>域置空,将指向它的<code>next</code>指針指向其後繼節點,以友善GC

好了,到這裡為止,我們就完成了對AQS基本結構的分析。這裡如果有不懂的地方,可以暫時跳過,等看完後續部落格再回頭看這篇,應該就能明白了 下一篇我們會逐漸剖析AQS如何實作對資源的擷取和釋放,go go go!

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

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

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

願歸來仍是少年!

    作者:酒冽

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

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

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