AQS的原理淺析

本文是《Java特種兵》的樣章,本書即將由工業出版社出版

AQS的全稱爲(AbstractQueuedSynchronizer),這個類也是在java.util.concurrent.locks下面。這個相似乎很不容易看懂,由於它僅僅是提供了一系列公共的方法,讓子類來調用。那麼要理解意思,就得從子類下手,反過來看才容易看懂。以下圖所示:


圖 5-15 AQS的子類實現

這麼多類,咱們看那一個?剛剛提到過鎖(Lock),咱們就從鎖開始吧。這裏就先以ReentrantLock排它鎖爲例開始展開講解如何利用AQS的,而後再簡單介紹讀寫鎖的要點(讀寫鎖自己的實現十分複雜,要徹底說清楚須要大量的篇幅來講明)。
首先來看看ReentrantLock的構造方法,它的構造方法有兩個,以下圖所示:

圖 5-16 排它鎖的構造方法
很顯然,對象中有一個屬性叫sync,有兩種不一樣的實現類,默認是「NonfairSync」來實現,而另外一個「FairSync」它們都是排它鎖的內部類,不論用那一個都能實現排它鎖,只是內部可能有點原理上的區別。先以「NonfairSync」類爲例,它的lock()方法是如何實現的呢?

圖 5-17 排它鎖的lock方法
lock()方法先經過CAS嘗試將狀態從0修改成1。若直接修改爲功,前提條件天然是鎖的狀態爲0,則直接將線程的OWNER修改成當前線程,這是一種理想狀況,若是併發粒度設置適當也是一種樂觀狀況。
若上一個動做未成功,則會間接調用了acquire(1)來繼續操做,這個acquire(int)方法就是在AbstractQueuedSynchronizer當中了。這個方法表面上看起來簡單,但真實狀況比較難以看懂,由於第一次看這段代碼可能不知道它要作什麼!不急,一步一步來分解。
首先看tryAcquire(arg)這裏的調用(固然傳入的參數是1),在默認的「NonfairSync」實現類中,會這樣來實現:


媽呀,這代碼好費勁,胖哥第一回看也是以爲這樣,細心看看也不是想象當中那麼難:

○ 首先獲取這個鎖的狀態,若是狀態爲0,則嘗試設置狀態爲傳入的參數(這裏就是1),若設置成功就表明本身獲取到了鎖,返回true了。狀態爲0設置1的動做在外部就有作過一次,內部再一次作只是提高几率,並且這樣的操做相對鎖來說不佔開銷。
○ 若是狀態不是0,則斷定當前線程是否爲排它鎖的Owner,若是是Owner則嘗試將狀態增長acquires(也就是增長1),若是這個狀態值越界,則會拋出異常提示,若沒有越界,將狀態設置進去後返回true(實現了相似於偏向的功能,可重入,可是無需進一步徵用)。
○ 若是狀態不是0,且自身不是owner,則返回false。

回到圖 5-17中對tryAcquire()的調用斷定中是經過if(!tryAcquire())做爲第1個條件的,若是返回true,則斷定就不會成立了,天然後面的acquireQueued動做就不會再執行了,若是發生這樣的狀況是最理想的。
不管多麼樂觀,徵用是必然存在的,若是徵用存在則owner天然不會是本身,tryAcquire()方法會返回false,接着就會再調用方法:acquireQueued(addWaiter(Node.EXCLUSIVE), arg)作相關的操做。
這個方法的調用的代碼更很差懂,須要從裏往外看,這裏的Node.EXCLUSIVE是節點的類型,看名稱應該清楚是排它類型的意思。接着調用addWaiter()來增長一個排它鎖類型的節點,這個addWaiter()的代碼是這樣寫的:

圖 5-19 addWaiter的代碼
這裏建立了一個Node的對象,將當前線程和傳入的Node.EXCLUSIVE傳入,也就是說Node節點理論上包含了這兩項信息。代碼中的tail是AQS的一個屬性,剛開始的時候確定是爲null,也就是不會進入第一層if斷定的區域,而直接會進入enq(node)的代碼,那麼直接來看看enq(node)的代碼。

看到了tail就應該猜到了AQS是鏈表吧,沒錯,並且它還應該有一個head引用來指向鏈表的頭節點,AQS在初始化的時候head、tail都是null,在運行時來回移動。此時,咱們最少至少知道AQS是一個基於狀態(state)的鏈表管理方式。



圖 5-20 enq(Node)的源碼
這段代碼就是鏈表的操做,某些同窗可能很牛,一下就看懂了,某些同窗一掃而過以爲知道大概就能夠了,某些同窗可能會莫不着頭腦。胖哥爲了給第三類同窗來「開開葷」,簡單講解下這個代碼。
首先這個是一個死循環,並且自己沒有鎖,所以能夠有多個線程進來,假如某個線程進入方法,此時head、tail都是null,天然會進入if(t == null)所在的代碼區域,這部分代碼會建立一個Node出來名字叫h,這個Node沒有像開始那樣給予類型和線程,很明顯是一個空的Node對象,而傳入的Node對象首先被它的next引用所指向,此時傳入的node和某一個線程建立的h對象以下圖所示。

圖 5-21 臨時的h對象建立後的與傳入的Node指向關係
剛纔咱們很理想的認爲只有一個線程會出現這種狀況,若是有多個線程併發進入這個if斷定區域,可能就會同時存在多個這樣的數據結構,在各自造成數據結構後,多個線程都會去作compareAndSetHead(h)的動做,也就是嘗試將這個臨時h節點設置爲head,顯然併發時只有一個線程會成功,所以成功的那個線程會執行tail = node的操做,整個AQS的鏈表就成爲:


圖 5-22 AQS被第一個請求成功的線程初始化後
有一個線程會成功修改head和tail的值,其它的線程會繼續循環,再次循環就不會進入if (t == null)的邏輯了,而會進入else語句的邏輯中。
在else語句所在的邏輯中,第一步是node.prev = t,這個t就是tail的臨時值,也就是首先讓嘗試寫入的node節點的prev指針指向原來的結束節點,而後嘗試經過CAS替換掉AQS中的tail的內容爲當前線程的Node,不管有多少個線程併發到這裏,依然只會有一個能成功,成功者執行t.next = node,也就是讓原先的tail節點的next引用指向如今的node,如今的node已經成爲了最新的結束節點,不成功者則會繼續循環。
簡單使用圖解的方式來講明,3個步驟以下所示,以下圖所示:



圖 5-23 插入一個節點步驟先後動做
插入多個節點的時候,就以此類推了哦,總之節點都是在鏈表尾部寫入的,並且是線程安全的。
知道了AQS大體的寫入是一種雙向鏈表的插入操做,但插入鏈表節點對鎖有何用途呢,咱們還得退回到前面圖 5-19的代碼中addWaiter方法最終返回了要寫入的node節點, 再回退到圖5-17中所在的代碼中須要將這個返回的node節點做爲acquireQueued方法入口參數,並傳入另外一個參數(依然是1),看看它裏面到底作了些什麼?請看下圖:


圖 5-24 acquireQueued的方法內容
這裏也是一個死循環,除非進入if(p == head && tryAcquire(arg))這個斷定條件,而p爲node.predcessor()獲得,這個方法返回node節點的前一個節點,也就是說只有當前一個節點是head的時候,進一步嘗試經過tryAcquire(arg)來徵用纔有機會成功。tryAcquire(arg)這個方法咱們前面介紹過,成立的條件爲:鎖的狀態爲0,且經過CAS嘗試設置狀態成功或線程的持有者自己是當前線程纔會返回true,咱們如今來詳細拆分這部分代碼。
○ 若是這個條件成功後,發生的幾個動做包含:
(1) 首先調用setHead(Node)的操做,這個操做內部會將傳入的node節點做爲AQS的head所指向的節點。線程屬性設置爲空(由於如今已經獲取到鎖,再也不須要記錄下這個節點所對應的線程了),再將這個節點的perv引用賦值爲null。
(2) 進一步將的前一個節點的next引用賦值爲null。
在進行了這樣的修改後,隊列的結構就變成了如下這種狀況了,經過這樣的方式,就可讓執行完的節點釋放掉內存區域,而不是無限制增加隊列,也就真正造成FIFO了:

圖 5-25 CAS成功獲取鎖後,隊列的變化
○ 若是這個斷定條件失敗
會首先斷定:「shouldParkAfterFailedAcquire(p , node)」,這個方法內部會斷定前一個節點的狀態是否爲:「Node.SIGNAL」,如果則返回true,若不是都會返回false,不過會再作一些操做:斷定節點的狀態是否大於0,若大於0則認爲被「CANCELLED」掉了(咱們沒有說明幾個狀態的值,不過大於0的只可能被CANCELLED的狀態),所以會從前一個節點開始逐步循環找到一個沒有被「CANCELLED」節點,而後與這個節點的next、prev的引用相互指向;若是前一個節點的狀態不是大於0的,則經過CAS嘗試將狀態修改成「Node.SIGNAL」,天然的若是下一輪循環的時候會返回值應該會返回true。
若是這個方法返回了true,則會執行:「parkAndCheckInterrupt()」方法,它是經過LockSupport.park(this)將當前線程掛起到WATING狀態,它須要等待一箇中斷、unpark方法來喚醒它,經過這樣一種FIFO的機制的等待,來實現了Lock的操做。
相應的,能夠本身看看FairSync實現類的lock方法,其實區別不大,有些細節上的區別可能會決定某些特定場景的需求,你也能夠本身按照這樣的思路去實現一個自定義的鎖。
接下來簡單看看unlock()解除鎖的方式,若是獲取到了鎖不釋放,那天然就成了死鎖,因此必需要釋放,來看看它內部是如何釋放的。一樣從排它鎖(ReentrantLock)中的unlock()方法開始,請先看下面的代碼截圖:


圖 5-26 unlock方法間接調用AQS的release(1)來完成
經過tryRelease(int)方法進行了某種斷定,若它成立則會將head傳入到unparkSuccessor(Node)方法中並返回true,不然返回false。首先來看看tryRelease(int)方法,以下圖所示:


圖 5-27 tryRelease(1)方法
這個動做能夠認爲就是一個設置鎖狀態的操做,並且是將狀態減掉傳入的參數值(參數是1),若是結果狀態爲0,就將排它鎖的Owner設置爲null,以使得其它的線程有機會進行執行。
在排它鎖中,加鎖的時候狀態會增長1(固然能夠本身修改這個值),在解鎖的時候減掉1,同一個鎖,在能夠重入後,可能會被疊加爲二、三、4這些值,只有unlock()的次數與lock()的次數對應纔會將Owner線程設置爲空,並且也只有這種狀況下才會返回true。
這一點你們寫代碼要注意了哦,若是是在循環體中lock()或故意使用兩次以上的lock(),而最終只有一次unlock(),最終可能沒法釋放鎖。在本書的src/chapter05/locks/目錄下有相應的代碼,你們能夠自行測試的哦。
在方法unparkSuccessor(Node)中,就意味着真正要釋放鎖了,它傳入的是head節點(head節點是已經執行完的節點,在後面闡述這個方法的body的時候都叫head節點),內部首先會發生的動做是獲取head節點的next節點,若是獲取到的節點不爲空,則直接經過:「LockSupport.unpark()」方法來釋放對應的被掛起的線程,這樣一來將會有一個節點喚醒後繼續進入圖 5-24中的循環進一步嘗試tryAcquire()方法來獲取鎖,可是也未必能徹底獲取到哦,由於此時也可能有一些外部的請求正好與之徵用,並且還奇蹟般的成功了,那這個線程的運氣就有點悲劇了,不過一般樂觀認爲不會每一次都那麼悲劇。
再看看共享鎖,從前面的排它鎖能夠看得出來是用一個狀態來標誌鎖的,而共享鎖也不例外,可是Java不但願去定義兩個狀態,因此它與排它鎖的第一個區別就是在鎖的狀態上,它用int來標誌鎖的狀態,int有4個字節,它用高16位標誌讀鎖(共享鎖),低16位標誌寫鎖(排它鎖),高16位每次增長1至關於增長65536(經過1 << 16獲得),天然的在這種讀寫鎖中,讀鎖和寫鎖的個數都不能超過65535個(條件是每次增長1的,若是遞增是跳躍的將會更少)。在計算讀鎖數量的時候將狀態左移16位,而計算排它鎖會與65535「按位求與」操做,以下圖所示。


圖 5-28 讀寫鎖中的數量計算及限制
寫鎖的功能與「ReentrantLock」基本一致,區域在於它會在tryAcquire操做的時候,斷定狀態的時候會更加複雜一些(所以有些時候它的性能未必好)。
讀鎖也會寫入隊列,Node的類型被改成:「Node.SHARED」這種類型,lock()時候調用的是AQS的acquireShared(int)方法,進一步調用tryAcquireShared()操做裏面只須要檢測是否有排它鎖,若是沒有則能夠嘗試經過CAS修改鎖的狀態,若是沒有修改爲功,則會自旋這個動做(可能會有不少線程在這自旋開銷CPU)。若是這個自旋的過程當中檢測到排它鎖競爭成功,那麼tryAcquireShared()會返回-1,從而會走如排它鎖的Node相似的流程,可能也會被park住,等待排它鎖相應的線程最終調用unpark()動做來喚醒。
這就是Java提供的這種讀寫鎖,不過這並非共享鎖的詮釋,在共享鎖裏面也有多種機制 ,或許這種讀寫鎖只是其中一種而已。在這種鎖下面,讀和寫的操做自己是互斥的,可是讀能夠多個一塊兒發生。這樣的鎖理論上是很是適合應用在「讀多寫少」的環境下(固然咱們所講的讀多寫少是讀的比例遠遠大於寫,而不是多一點點),理論上講這樣鎖徵用的粒度會大大下降,同時系統的瓶頸會減小,效率獲得整體提高。
在本節中咱們除了學習到AQS的內在,還應看到Java經過一個AQS隊列解決了許多問題,這個是Java層面的隊列模型,其實咱們也能夠利用許多隊列模型來解決本身的問題,甚至於能夠改寫模型模型來知足本身的需求,在本章的5.6.1節中將會詳細介紹。
關於Lock及AQS的一些補充:
一、 Lock的操做不只僅侷限於lock()/unlock(),由於這樣線程可能進入WAITING狀態,這個時候若是沒有unpark()就無法喚醒它,可能會一直「睡」下去,能夠嘗試用tryLock()、tryLock(long , TimeUnit)來作一些嘗試加鎖或超時來知足某些特定場景的須要。例若有些時候發現嘗試加鎖沒法加上,先釋放已經成功對其它對象添加的鎖,過一小會再來嘗試,這樣在某些場合下能夠避免「死鎖」哦。
二、 lockInterruptibly() 它容許拋出InterruptException異常,也就是當外部發起了中斷操做,程序內部有可能會拋出這種異常,可是並非絕對會拋出異常的,你們仔細看看代碼便清楚了。
三、 newCondition()操做,是返回一個Condition的對象,Condition只是一個接口,它要求實現await()、awaitUninterruptibly()、awaitNanos(long)、await(long , TimeUnit)、awaitUntil(Date)、signal()、signalAll()方法,AbstractQueuedSynchronizer中有一個內部類叫作ConditionObject實現了這個接口,它也是一個相似於隊列的實現,具體能夠參考源碼。大多數狀況下能夠直接使用,固然以爲本身比較牛逼的話也能夠參考源碼本身來實現。
四、 在AQS的Node中有每一個Node本身的狀態(waitStatus),咱們這裏概括一下,分別包含:
SIGNAL 從前面的代碼狀態轉換能夠看得出是前面有線程在運行,須要前面線程結束後,調用unpark()方法才能激活本身,值爲:-1
CANCELLED 當AQS發起取消或fullyRelease()時,會是這個狀態。值爲1,也是幾個狀態中惟一一個大於0的狀態,因此前面斷定狀態大於0就基本等價因而CANCELLED的意思。
CONDITION 線程基於Condition對象發生了等待,進入了相應的隊列,天然也須要Condition對象來激活,值爲-2。
PROPAGATE 讀寫鎖中,當讀鎖最開始沒有獲取到操做權限,獲得後會發起一個doReleaseShared()動做,內部也是一個循環,當斷定後續的節點狀態爲0時,嘗試經過CAS自旋方式將狀態修改成這個狀態,表示節點能夠運行。
狀態0 初始化狀態,也表明正在嘗試去獲取臨界資源的線程所對應的Node的狀態。java

相關文章
相關標籤/搜索