AQS簡介與源碼剖析

AbstractQueuedSynchronizer,簡稱AQS,是Doug Lea的大型創作的用戶構建鎖或其他同步組件(信號量,事件等)的基礎框架類。


java.util.concurrent併發包中的工具類的內部實現都依賴於AQS,如常用的ReentrantLock,ReentrantWriteLock,CountDownLatch等的核心都是AQS,雖然它們都依賴AQS,但是通過AQS實現的功能卻是不同的。


同步器的主要使用方式是繼承,子類通過繼承同步器並實現它的抽象方法來管理同步狀態,它簡化了鎖的實現方式,屏蔽了同步狀態管理,線程的排隊,等待與喚醒等底層操作。


AQS使用了一個INT型成員變量狀態來表示線程的同步狀態,通過內置的同步隊列(FIFO雙向隊列)來完成管理線程同步狀態的工作,一旦當前線程沒有競爭到鎖,同步隊列會將當前線程以及線程的狀態放在一個節點節點中維護,並阻塞當前線程,等待被喚醒再次重新嘗試獲取鎖或者被取消等待。


我們來看下節點中存放了什麼:

節點是構成同步隊列的基礎,其中存放了獲取同步狀態失敗的線程引用,線程狀態,前驅節點,後繼節點,節點屬性(共享,獨佔)等,同步隊列的基本結構圖如下:


同步器中存放了頭節點,尾節點,沒有獲取到鎖的線程會加入到同步隊列的尾部,頭節點是獲取同步狀態成功的節點,頭節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點。


本文以JDK8來分析AQS的實現原理,本章將介紹獨佔鎖的獲取與釋放,廢話不多說了直接源碼,AQS是一個抽象類繼承了AbstractQueuedSynchronizer

這個類主要有如下幾個功能:

如圖1所示,提供了一個給子類使用的構造函數

2,設置當前獨佔同步狀態的線程

3,獲取當前獨佔同步狀態的線程


AQS成員變量:


獨佔式獲取同步狀態

調用AQS的獲取方法獲取鎖

如果我們看過的ReentrantLock這個類會發現一般精氨酸的值等於1,如果的tryAcquire(ARG)爲真這個方法就執行完畢了,說明當前線程獲取到了鎖


這裏我們用的ReentrantLock中覆寫該方法來解釋


1,如果狀態= 0,說明沒有線程持有鎖,然後調用hasQueuedPredecessors()看有沒有其他線程在同步隊列中等待,如果方法該返回假。如果沒有其他線程在等待,則CAS原子操作設置狀態,如果設置成功則說明當前線程獲取到了鎖,然後將該線程設置爲獨佔模式。

2,如果走到else if語句中,如果是表示當前線程擁有鎖,這個時候鎖重入了,然後設置state value,這裏也就視解釋了前面說的state爲什麼會大於0的時候表示線程佔有了鎖

3,如果上述都不滿足則,返回false,說明當前線程沒有獲取到鎖,則程序走到acquireQueued(addWaiter(Node.EXCLUSIVE),arg),這個時候該線程就要被掛起了,放入到同步隊列中,等待前置節點的喚醒先看addWaiter方法:


如圖1所示,將當線程用一個節點節點來維護,如果尾節點不爲空,設置節點的前驅節點爲尾節點,通過CAS將節點設置成尾節點,然後將PRED的後繼節點指向到節點,形成了首尾相接。至此線程進入同步隊列,返回當前線程的節點節點。

2,如果尾節點爲空的話或者線程競爭入隊導致CAS失敗,則調用ENQ(節點)

這裏使用了自旋的方式進入隊列:

如圖1所示,如果尾節點爲空,說明整個隊列爲空,初始化一個節點,通過CAS將該節點設置爲頭節點,並將尾節點指向頭節點


2,再次循環的時候尾節點此時已經不爲空了,然後將節點的前驅節點爲之前的尾節點,然後通過CAS將當前線程節點設置爲尾節點,這裏說明下爲什麼要使用無限循環呢,因爲這個時候可能會有其他線程因爲沒有獲取到同步狀態來競爭插入隊尾,那麼當前線程就重複循環直到插入到隊尾爲止。然後返回隊尾的節點。

這時候再過過頭來看acquireQueued(addWaiter(Node.EXCLUSIVE),arg)方法:


當前線程在無限循環中嘗試獲取同步狀態,這裏結合下圖來解釋acquireQueued(addWaiter(Node.EXCLUSIVE),arg)


如圖1所示,假設當線程獲取同步狀態失敗,插入到了同步隊列的隊尾,我們假設稱爲之節點2,當前線程執行到acquireQueued,因爲節點2的前驅節點爲節點1,節點1節點不是頭節點,然後執行shouldParkAfterFailedAcquire(對,node)&& parkAndCheckInterrupt())去掛起線程,這個時候節點node2會一直自旋判斷其前驅節點是否爲head節點並等待被喚醒


2,此時另外一個線程也在執行acquireQueued,並且節點爲節點1,從上圖中看出節點1的前驅節點爲頭節點,然後嘗試獲取同步狀態,這個時候有人可能有疑問了,頭節點持有的同步可能沒有釋放啊,爲什麼節點1可以嘗試獲取同步狀態,這是因爲兩點:

1>因爲頭節點可能是在enq中初始化的,而新節點()會延遲初始化,這個時候還沒有其他線程持有這個初始化的節點,因此作爲隊頭可以嘗試去獲取。

2>這裏調用的tryAcquire(ARG)獲取同步狀態是爲了等待頭節點釋放同步狀態後喚醒後繼節點,節點1可以嘗試獲取同步狀態。


3,只有前驅節點是頭節點才能夠嘗試獲取鎖,因爲成功獲取到鎖的節點一定是頭節點,而頭節點的線程釋放了同步狀態之後,將會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點。這個方法其實是線程真正被喚醒和掛起的地方。


如圖4所示,如果前驅節點不是頭節點或者未成功獲取鎖則根據前驅節點和當前線程節點判斷是否要掛起。如果阻塞過程中被中斷,則置中斷標誌位爲true.acquireQueued的返回值代表的是是否被中斷,線程的中斷狀態爲假,如果發生中斷則要重新設置中斷狀態,會通過selfInterrupt設置回去,其實acquireQueued本身是不關心中斷狀態。真正關心中斷的在doAcquireInterruptibly中


5,如果前驅節點不是頭節點或者當前線程獲取同步狀態失敗則會走到if(shouldParkAfterFailedAcquire(p,node)&& parkAndCheckInterrupt()))...中,來看做了什麼:

這裏會判斷前驅節點的等待狀態:

1,waitStatus = -1說明前驅節點狀態正常,當前節點則需要被掛起

2,waitStatus> 0前驅節點狀態爲取消(CANCELED狀態),則向前遍歷,直到找到前驅節點是非取消狀態,更新當前節點的前驅爲往前第一個非取消節點。

3,waitStatus不爲上面兩種狀態,那麼只可能爲0(新Node()時的waitStatus爲0), - 2(條件狀態等到其他線程調用信號方法後該節點會從等待隊列轉移到同步同列中), - 3(PROPAGATE:表示下一次共享狀態會被無條件的傳播開),當爲這三種狀態的時候將前驅節點設置爲SIGNAL狀態,當前線程會之後會再次回到循環並嘗試獲取鎖。


如果shouldParkAfterFailedAcquire(p,node)返回說明當前線程需要掛起,等待前驅節點的喚醒,在哪裏掛起呢,這裏調用了LockSupport來喚醒線程


最後我們來看下acquireQueued中最終塊:如果(失敗)cancelAcquire(節點); 這裏好像永遠都走不到因爲:失敗似乎永遠都不可能爲真這裏看着有點像是模版代碼一樣,目的是由於響應中斷或者其他的異常情況會導致執行cancelAuquire:主要用於喚醒後繼節點和取消某個節點獲取同步狀態


獨佔式同步狀態的釋放

當前線程獲取同步狀態並執行了相應的業務邏輯之後,就需要釋放同步狀態,避免長期持有鎖造成的資源浪費以及其他線程的長時間阻塞導致系統性能的問題。通過調用AQS的release(int arg )方法可以釋放同步狀態,會喚醒後繼節點嘗試獲取同步狀態。

如果tryRelease(ARG)爲真則,頭節點不爲空並且頭節點的狀態不爲0(這裏爲什麼是h.waitStatus!= 0,因爲頭節點的狀態肯定不會爲-1,然後頭節點可能是new node()出來的這個時候waitStatus爲0)則喚醒後繼線程。


總結

獨佔式同步狀態獲取流程,也就是acquire(int arg)方法調用流程


參考:

Doug Lea:“Java併發編程實戰”

方騰飛,魏鵬,程曉明:“Java併發編程的藝術”

CSDN文章同步會慢些,歡迎關注微信公衆號:挨踢男孩


    



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