天天看點

一文帶你學會AQS和并發工具類的關系

  AQS(AbstractQueuedSynchronizer)是JAVA中衆多鎖以及并發工具的基礎,其底層采用樂觀鎖,大量使用了CAS操作, 并且在沖突時,采用自旋方式重試,以實作輕量級和高效地擷取鎖。

  提供一個架構,用于實作依賴于先進先出(FIFO)等待隊列的阻塞鎖和相關的同步器(semaphore等)。 此類旨在為大多數依賴單個原子int值表示狀态的同步器提供有用的基礎。 子類必須定義更改此狀态的受保護方法,并定義該狀态對于擷取或釋放此對象而言意味着什麼。 鑒于這些,此類中的其他方法将執行所有排隊和阻塞機制。 子類可以維護其他狀态字段,但是相對于同步,僅跟蹤使用方法getState,setState和compareAndSetState操作的原子更新的int值。

  此類支援預設獨占模式和共享模式之一或兩者。 當以獨占方式進行擷取時,其他線程嘗試進行的擷取将無法成功。 由多個線程擷取的共享模式可能(但不一定)成功。 該類不“了解”這些差異,當共享模式擷取成功時,下一個等待線程(如果存在)還必須确定它是否也可以擷取。 在不同模式下等待的線程共享相同的FIFO隊列。 通常,實作子類僅支援這些模式之一,但例如可以在ReadWriteLock發揮作用。 僅支援獨占模式或僅支援共享模式的子類無需定義支援未使用模式的方法。

  state是整個工具的核心,通常整個工具都是在設定和修改狀态,很多方法的操作都依賴于目前狀态是什麼。由于狀态是全局共享的,一般會被設定成volatile類型,為了保證其修改的可見性;

  AQS中的隊列是CLH變體的虛拟雙向隊列(FIFO),AQS是通過将每條請求共享資源的線程封裝成一個節點來實作鎖的配置設定。隊列采用的是悲觀鎖的思想,表示目前所等待的資源,狀态或者條件短時間内可能無法滿足。是以,它會将目前線程包裝成某種類型的資料結構,扔到一個等待隊列中,當一定條件滿足後,再從等待隊列中取出。

  CAS操作是最輕量的并發處理,通常我們對于狀态的修改都會用到CAS操作,因為狀态可能被多個線程同時修改,CAS操作保證了同一個時刻,隻有一個線程能修改成功,進而保證了線程安全。CAS采用的是樂觀鎖的思想,是以常常伴随着自旋,如果發現目前無法成功地執行CAS,則不斷重試,直到成功為止。

  要将此類用作同步器的基礎,請使用getState,setState或compareAndSetState檢查或修改同步狀态,以重新定義以下方法(如适用):

tryAcquire

獨占方式,arg為擷取鎖的次數,嘗試擷取資源,成功則傳回True,失敗則傳回False。

tryRelease

獨占方式,arg為釋放鎖的次數,嘗試釋放資源,成功則傳回True,失敗則傳回False。

tryAcquireShared

共享方式,arg為擷取鎖的次數,嘗試擷取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。

tryReleaseShared

共享方式,arg為釋放鎖的次數,嘗試釋放資源,如果釋放後允許喚醒後續等待結點傳回True,否則傳回False。

isHeldExclusively

該線程是否正在獨占資源。隻有用到Condition才需要去實作它。

  預設情況下,這些方法中的每一個都會引發UnsupportedOperationException 。 這些方法的實作必須在内部是線程安全的,并且通常應簡短且不阻塞。 定義這些方法是使用此類的唯一受支援的方法。 所有其他方法都被聲明為final方法,因為它們不能獨立變化。

  AQS中維護了一個名為state的字段,意為同步狀态,是由volatile修飾的,用于展示目前臨界資源的獲鎖情況。

下面提供了幾個通路這個state字段的方法:

傳回同步狀态的目前值。 此操作具有volatile讀取的記憶體語義

設定同步狀态的值。 此操作具有volatile寫操作的記憶體語義。

  如果目前狀态值等于期望值,則以原子方式将同步狀态設定為給定的更新值。 此操作具有volatile讀寫的記憶體語義

  這幾個方法都是Final修飾的,說明子類中無法重寫它們。我們可以通過修改State字段表示的同步狀态來實作多線程的獨占模式和共享模式state的值即表示了鎖的狀态,state為0表示鎖沒有被占用,state大于0表示目前已經有線程持有該鎖,這裡之是以說大于0而不說等于1是因為可能存在可重入的情況。你可以把state變量當做是目前持有該鎖的線程數量。

exclusiveOwnerThread 屬性的值即為目前持有鎖的線程獨占模式擷取鎖流程:

一文帶你學會AQS和并發工具類的關系

共享模式擷取鎖流程:

一文帶你學會AQS和并發工具類的關系

AQS中最基本的資料結構是Node,Node即為CLH變體隊列中的節點。

AQS中CLH變體的虛拟雙向隊列(FIFO),AQS是通過将每條請求共享資源的線程封裝成一個節點來實作鎖的配置設定。

一文帶你學會AQS和并發工具類的關系

  在AQS中的隊列是一個FIFO隊列,它的head節點永遠是一個虛拟結點(dummy node), 它不代表任何線程,是以head所指向的Node的thread屬性永遠是null。但是我們不會在建構過程中建立它們,因為如果沒有争用,這将是浪費時間。 而是構造節點,并在第一次争用時設定頭和尾指針。隻有從次頭節點往後的所有節點才代表了所有等待鎖的線程。也就是說,在目前線程沒有搶到鎖被包裝成Node扔到隊列中時,即使隊列是空的,它也會排在第二個,我們會在它的前面建立一個虛拟節點。

構造函數源代碼

ReentrantLock 裡面有三個内部類:

一個是抽象的 Sync 實作了 AbstractQueuedSynchronizer

NonfairSync 繼承了 Sync

FairSync 繼承了 Sync

一文帶你學會AQS和并發工具類的關系

ReentrantLock 種擷取鎖的方法

ReentrantLock 的非公平鎖實作

compareAndSetState(0,1)

stateOffset 為AQS種維護的state屬性的偏移量

一文帶你學會AQS和并發工具類的關系

setExclusiveOwnerThread(Thread.currentThread());

acquire(1); 調用的是AQS 中的acquire(int arg) 方法

tryAcquire(arg) 該方法是protected的由子類去具體實作的

一文帶你學會AQS和并發工具類的關系
一文帶你學會AQS和并發工具類的關系

  我們需要看的是NonfairSync中實作的tryAcquire方法,裡面又調用了nonfairTryAcquire方法,再進去看看

nonfairTryAcquire(int acquires) 方法實作

acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法實作,先看裡addWaiter(Node.EXCLUSIVE)方法注意:Node.EXCLUSIVE 此時是空值,是以mode 就是空的,是以此時建立的Node節點中的nextWaiter是空值。

如果CLH隊列的尾部節點為空值的話,執行enq(node)方法

檢視 acquireQueued 方法實作

  為什麼前面擷取鎖失敗了, 這裡還要再次嘗試擷取鎖呢?首先, 這裡再次嘗試擷取鎖是基于一定的條件的,即:目前節點的前驅節點就是HEAD節點,因為我們知道,head節點就是個虛拟節點,它不代表任何線程,或者代表了持有鎖的線程,如果目前節點的前驅節點就是head節點,那就說明目前節點已經是排在整個等待隊列最前面的了。

setHead(node); 方法

  可以看出,這個方法的本質是丢棄原來的head,将head指向已經獲得了鎖的node。但是接着又将該node的thread屬性置為null了,這某種意義上導緻了這個新的head節點又成為了一個虛拟節點,它不代表任何線程。為什麼要這樣做呢,因為在tryAcquire調用成功後,exclusiveOwnerThread屬性就已經記錄了目前擷取鎖的線程了,此處沒有必要再記錄。這某種程度上就是将目前線程從等待隊列裡面拿出來了,是一個變相的出隊操作。shouldParkAfterFailedAcquire(Node pred, Node node)方法

如果為前驅節點的waitStatus值為 Node.SIGNAL 則直接傳回 true

如果為前驅節點的waitStatus值為 Node.CANCELLED (ws > 0), 則跳過那些節點, 重新尋找正常等待中的前驅節點,然後排在它後面,傳回false

其他情況, 将前驅節點的狀态改為 Node.SIGNAL, 傳回false

acquireQueued方法中的Finally代碼

非公平鎖擷取鎖成功的流程圖

一文帶你學會AQS和并發工具類的關系

非公平鎖擷取鎖失敗的流程圖

一文帶你學會AQS和并發工具類的關系

  嘗試釋放此鎖。如果目前線程是此鎖的持有者,則保留計數将減少。 如果保持計數現在為零,則釋放鎖定。 如果目前線程不是此鎖的持有者,則抛出IllegalMonitorStateException。

sync.release(1) 調用的是AbstractQueuedSynchronizer中的release方法

分析tryRelease(arg)方法

一文帶你學會AQS和并發工具類的關系

tryRelease(arg)該方法調用的是ReentrantLock中

  如果頭節點不為空,并且waitStatus != 0,喚醒後續節點如果存在的話。這裡的判斷條件為什麼是h != null && h.waitStatus != 0?

  因為h == null的話,Head還沒初始化。初始情況下,head == null,第一個節點入隊,Head會被初始化一個虛拟節點。是以說,這裡如果還沒來得及入隊,就會出現head == null 的情況。

h != null && waitStatus == 0 表明後繼節點對應的線程仍在運作中,不需要喚醒

h != null && waitStatus < 0 表明後繼節點可能被阻塞了,需要喚醒

  為什麼要從後往前找第一個非Cancelled的節點呢?看一下addWaiter方法

  我們從這裡可以看到,節點入隊并不是原子操作,也就是說,node.prev = pred, compareAndSetTail(pred, node) 這兩個地方可以看作Tail入隊的原子操作,但是此時pred.next = node;還沒執行,如果這個時候執行了unparkSuccessor方法,就沒辦法從前往後找了,是以需要從後往前找。還有一點原因,在産生CANCELLED狀态節點的時候,先斷開的是Next指針,Prev指針并未斷開,是以也是必須要從後往前周遊才能夠周遊完全部的Node。

是以,如果是從前往後找,由于極端情況下入隊的非原子操作和CANCELLED節點産生過程中斷開Next指針的操作,可能會導緻無法周遊所有的節點。是以,喚醒對應的線程後,對應的線程就會繼續往下執行。

  由于篇幅較長公平鎖的實作在下一篇的部落格中講述,謝謝大家的關注和支援!有問題希望大家指出,共同進步!!!

AQS

繼續閱讀