書籍中的一個小樣章-Java並發編程AQS原理淺析
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”實現類中,會這樣來實現:

圖 5-18tryAcquire的代碼
媽呀,這代碼好費勁,胖哥第一回看也是覺得這樣,細心看看也不是想象當中那麼難:
○ 首先獲取這個鎖的狀態,如果狀態為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-19addWaiter的代碼
這裏創建了一個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的鏈表就成為:
有一個線程會成功修改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個步驟如下所示,如下圖所示:
插入多個節點的時候,就以此類推了哦,總之節點都是在鏈表尾部寫入的,而且是線程安全的。
知道了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了:

○如果這個判定條件失敗
會首先判定:“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-27tryRelease(1)方法
這個動作可以認為就是一個設置鎖狀態的操作,而且是將狀態減掉傳入的參數值(參數是1),如果結果狀態為0,就將排它鎖的Owner設置為null,以使得其它的線程有機會進行執行。
在排它鎖中,加鎖的時候狀態會增加1(當然可以自己修改這個值),在解鎖的時候減掉1,同一個鎖,在可以重入後,可能會被疊加為2、3、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修改鎖的狀態,如果沒有修改成功,則會自旋這個動作。如果這個自旋的過程中檢測到排它鎖競爭成功,那麼tryAcquireShared()會返回-1,從而會走如排它鎖的Node類似的流程,可能也會被park住,等待排它鎖相應的線程最終調用unpark()動作來喚醒。
這就是讀寫鎖,不過共享鎖裏麵也有多種機製,我們隻是說了一種而已。在這種鎖下麵,讀和寫的操作本身是互斥的,但是讀可以多個一起發生。這樣的鎖非常適合應用在“讀多寫少”的環境下,這樣鎖征用的粒度會大大降低,同時係統的瓶頸會減少,效率得到總體提升。
在本節中我們除了學習到AQS的內在,還應看到Java通過一個AQS隊列解決了許多問題,這個是Java層麵的隊列模型,其實我們也可以利用許多隊列模型來解決自己的問題,甚至於可以改寫模型模型來滿足自己的需求,在本章的5.6.1節中將會詳細介紹。
關於Lock及AQS的一些補充: 1、 Lock的操作不僅僅局限於lock()/unlock(),因為這樣線程可能進入WAITING狀態,這個時候如果沒有unpark()就沒法喚醒它,可能會一直“睡”下去,可以嚐試用tryLock()、tryLock(long , TimeUnit)來做一些嚐試加鎖或超時來滿足某些特定場景的需要。例如有些時候發現嚐試加鎖無法加上,先釋放已經成功對其它對象添加的鎖,過一小會再來嚐試,這樣在某些場合下可以避免“死鎖”哦。 2、 lockInterruptibly() 它允許拋出InterruptException異常,也就是當外部發起了中斷操作,程序內部有可能會拋出這種異常,但是並不是絕對會拋出異常的,大家仔細看看代碼便清楚了。 3、 newCondition()操作,是返回一個Condition的對象,Condition隻是一個接口,它要求實現await()、awaitUninterruptibly()、awaitNanos(long)、await(long , TimeUnit)、awaitUntil(Date)、signal()、signalAll()方法,AbstractQueuedSynchronizer中有一個內部類叫做ConditionObject實現了這個接口,它也是一個類似於隊列的實現,具體可以參考源碼。大多數情況下可以直接使用,當然覺得自己比較牛逼的話也可以參考源碼自己來實現。 4、 在AQS的Node中有每個Node自己的狀態(waitStatus),我們這裏歸納一下,分別包含: SIGNAL 從前麵的代碼狀態轉換可以看得出是前麵有線程在運行,需要前麵線程結束後,調用unpark()方法才能激活自己,值為:-1 CANCELLED 當AQS發起取消或fullyRelease()時,會是這個狀態。值為1,也是幾個狀態中唯一一個大於0的狀態,所以前麵判定狀態大於0就基本等價於是CANCELLED的意思。 CONDITION 線程基於Condition對象發生了等待,進入了相應的隊列,自然也需要Condition對象來激活,值為-2。 PROPAGATE 讀寫鎖中,當讀鎖最開始沒有獲取到操作權限,得到後會發起一個doReleaseShared()動作,內部也是一個循環,當判定後續的節點狀態為0時,嚐試通過CAS自旋方式將狀態修改為這個狀態,表示節點可以運行。 狀態0 初始化狀態,也代表正在嚐試去獲取臨界資源的線程所對應的Node的狀態。 |
博客上格式全部亂掉了,有點暈,不過書上格式比這個應該要清爽許多。
最後更新:2017-04-03 12:53:47