天天看點

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

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

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-15 AQS的子類實作

這麼多類,我們看那一個?剛剛提到過鎖(Lock),我們就從鎖開始吧。這裡就先以ReentrantLock排它鎖為例開始展開講解如何利用AQS的,然後再簡單介紹讀寫鎖的要點(讀寫鎖本身的實作十分複雜,要完全說清楚需要大量的篇幅來說明)。

首先來看看ReentrantLock的構造方法,它的構造方法有兩個,如下圖所示:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-16 排它鎖的構造方法

很顯然,對象中有一個屬性叫sync,有兩種不同的實作類,預設是“NonfairSync”來實作,而另一個“FairSync”它們都是排它鎖的内部類,不論用那一個都能實作排它鎖,隻是内部可能有點原理上的差別。先以“NonfairSync”類為例,它的lock()方法是如何實作的呢?

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-17 排它鎖的lock方法

lock()方法先通過CAS嘗試将狀态從0修改為1。若直接修改成功,前提條件自然是鎖的狀态為0,則直接将線程的OWNER修改為目前線程,這是一種理想情況,如果并發粒度設定适當也是一種樂觀情況。

若上一個動作未成功,則會間接調用了acquire(1)來繼續操作,這個acquire(int)方法就是在AbstractQueuedSynchronizer當中了。這個方法表面上看起來簡單,但真實情況比較難以看懂,因為第一次看這段代碼可能不知道它要做什麼!不急,一步一步來分解。

首先看tryAcquire(arg)這裡的調用(當然傳入的參數是1),在預設的“NonfairSync”實作類中,會這樣來實作:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

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

○ 首先擷取這個鎖的狀态,如果狀态為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()的代碼是這樣寫的:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-19 addWaiter的代碼

這裡建立了一個Node的對象,将目前線程和傳入的Node.EXCLUSIVE傳入,也就是說Node節點理論上包含了這兩項資訊。代碼中的tail是AQS的一個屬性,剛開始的時候肯定是為null,也就是不會進入第一層if判定的區域,而直接會進入enq(node)的代碼,那麼直接來看看enq(node)的代碼。

看到了tail就應該猜到了AQS是連結清單吧,沒錯,而且它還應該有一個head引用來指向連結清單的頭節點,AQS在初始化的時候head、tail都是null,在運作時來回移動。此時,我們最少至少知道AQS是一個基于狀态(state)的連結清單管理方式。

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-20 enq(Node)的源碼

這段代碼就是連結清單的操作,某些同學可能很牛,一下就看懂了,某些同學一掃而過覺得知道大概就可以了,某些同學可能會莫不着頭腦。胖哥為了給第三類同學來“開開葷”,簡單講解下這個代碼。

首先這個是一個死循環,而且本身沒有鎖,是以可以有多個線程進來,假如某個線程進入方法,此時head、tail都是null,自然會進入if(t == null)所在的代碼區域,這部分代碼會建立一個Node出來名字叫h,這個Node沒有像開始那樣給予類型和線程,很明顯是一個空的Node對象,而傳入的Node對象首先被它的next引用所指向,此時傳入的node和某一個線程建立的h對象如下圖所示。

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-21 臨時的h對象建立後的與傳入的Node指向關系

剛才我們很理想的認為隻有一個線程會出現這種情況,如果有多個線程并發進入這個if判定區域,可能就會同時存在多個這樣的資料結構,在各自形成資料結構後,多個線程都會去做compareAndSetHead(h)的動作,也就是嘗試将這個臨時h節點設定為head,顯然并發時隻有一個線程會成功,是以成功的那個線程會執行tail = node的操作,整個AQS的連結清單就成為:

Java并發程式設計- Lock和condition的實作細節 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個步驟如下所示,如下圖所示:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-23 插入一個節點步驟前後動作

插入多個節點的時候,就以此類推了哦,總之節點都是在連結清單尾部寫入的,而且是線程安全的。

知道了AQS大緻的寫入是一種雙向連結清單的插入操作,但插傳入連結表節點對鎖有何用途呢,我們還得退回到前面圖 5-19的代碼中addWaiter方法最終傳回了要寫入的node節點, 再回退到圖5-17中所在的代碼中需要将這個傳回的node節點作為acquireQueued方法入口參數,并傳入另一個參數(依然是1),看看它裡面到底做了些什麼?請看下圖:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 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了:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 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()方法開始,請先看下面的代碼截圖:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-26 unlock方法間接調用AQS的release(1)來完成

通過tryRelease(int)方法進行了某種判定,若它成立則會将head傳入到unparkSuccessor(Node)方法中并傳回true,否則傳回false。首先來看看tryRelease(int)方法,如下圖所示:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 5-27 tryRelease(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“按位求與”操作,如下圖所示。

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

圖 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的一些補充:

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的狀态。

Codition的實作

在java.util.concurrent包中,有兩個很特殊的工具類,Condition和ReentrantLock,使用過的人都知道,ReentrantLock(重入鎖)是jdk的concurrent包提供的一種獨占鎖的實作。它繼承自Dong Lea的 AbstractQueuedSynchronizer(同步器),确切的說是ReentrantLock的一個内部類繼承了AbstractQueuedSynchronizer,ReentrantLock隻不過是代理了該類的一些方法,可能有人會問為什麼要使用内部類在包裝一層? 我想是安全的關系,因為AbstractQueuedSynchronizer中有很多方法,還實作了共享鎖,Condition(稍候再細說)等功能,如果直接使ReentrantLock繼承它,則很容易出現AbstractQueuedSynchronizer中的API被誤用的情況。

言歸正傳,今天,我們讨論下Condition工具類的實作。

ReentrantLock和Condition的使用方式通常是這樣的:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

運作後,結果如下:

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

可以看到,

Condition的執行方式,是當線上程1中調用await方法後,線程1将釋放鎖,并且将自己沉睡,等待喚醒,

線程2擷取到鎖後,開始做事,完畢後,調用Condition的signal方法,喚醒線程1,線程1恢複執行。

以上說明Condition是一個多線程間協調通信的工具類,使得某個,或者某些線程一起等待某個條件(Condition),隻有當該條件具備( signal 或者 signalAll方法被帶調用)時 ,這些等待線程才會被喚醒,進而重新争奪鎖。

那,它是怎麼實作的呢?

首先還是要明白,reentrantLock.newCondition() 傳回的是Condition的一個實作,該類在AbstractQueuedSynchronizer中被實作,叫做newCondition()

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

它可以通路AbstractQueuedSynchronizer中的方法和其餘内部類( AbstractQueuedSynchronizer是個抽象類,至于他怎麼能通路,這裡有個很奇妙的點,後面我專門用demo說明 )

現在,我們一起來看下Condition類的實作,還是從上面的demo入手,

為了友善書寫,我将AbstractQueuedSynchronizer縮寫為AQS

當await被調用時,代碼如下:

01

public

final

void

await() 

throws

InterruptedException {

02

if

(Thread.interrupted())

03

throw

new

InterruptedException();

04

Node node = addConditionWaiter(); 

//将目前線程包裝下後,

05

//添加到Condition自己維護的一個連結清單中。

06

int

savedState = fullyRelease(node);

//釋放目前線程占有的鎖,從demo中看到,

07

//調用await前,目前線程是占有鎖的

08

09

int

interruptMode = 

;

10

while

(!isOnSyncQueue(node)) {

//釋放完畢後,周遊AQS的隊列,看目前節點是否在隊列中,

11

//不在 說明它還沒有競争鎖的資格,是以繼續将自己沉睡。

12

//直到它被加入到隊列中,聰明的你可能猜到了,

13

//沒有錯,在singal的時候加入不就可以了?

14

LockSupport.park(

this

);

15

if

((interruptMode = checkInterruptWhileWaiting(node)) != 

)

16

break

;

17

}

18

//被喚醒後,重新開始正式競争鎖,同樣,如果競争不到還是會将自己沉睡,等待喚醒重新開始競争。

19

if

(acquireQueued(node, savedState) && interruptMode != THROW_IE)

20

interruptMode = REINTERRUPT;

21

if

(node.nextWaiter != 

null

)

22

unlinkCancelledWaiters();

23

if

(interruptMode != 

)

24

reportInterruptAfterWait(interruptMode);

25

}

回到上面的demo,鎖被釋放後,線程1開始沉睡,這個時候線程因為線程1沉睡時,會喚醒AQS隊列中的頭結點,所是以線程2會開始競争鎖,并擷取到,等待3秒後,線程2會調用signal方法,“發出”signal信号,signal方法如下:

1

public

final

void

signal() {

2

if

(!isHeldExclusively())

3

throw

new

IllegalMonitorStateException();

4

Node first = firstWaiter; 

//firstWaiter為condition自己維護的一個連結清單的頭結點,

5

//取出第一個節點後開始喚醒操作

6

if

(first != 

null

)

7

doSignal(first);

8

}

說明下,其實Condition内部維護了等待隊列的頭結點和尾節點,該隊列的作用是存放等待signal信号的線程,該線程被封裝為Node節點後存放于此。

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

關鍵的就在于此,我們知道AQS自己維護的隊列是目前等待資源的隊列,AQS會在資源被釋放後,依次喚醒隊列中從前到後的所有節點,使他們對應的線程恢複執行。直到隊列為空。

而Condition自己也維護了一個隊列,該隊列的作用是維護一個等待signal信号的隊列,兩個隊列的作用是不同,事實上,每個線程也僅僅會同時存在以上兩個隊列中的一個,流程是這樣的:

1. 線程1調用reentrantLock.lock時,線程被加入到AQS的等待隊列中。

2. 線程1調用await方法被調用時,該線程從AQS中移除,對應操作是鎖的釋放。

3. 接着馬上被加入到Condition的等待隊列中,意味着該線程需要signal信号。

4. 線程2,因為線程1釋放鎖的關系,被喚醒,并判斷可以擷取鎖,于是線程2擷取鎖,并被加入到AQS的等待隊列中。

5.  線程2調用signal方法,這個時候Condition的等待隊列中隻有線程1一個節點,于是它被取出來,并被加入到AQS的等待隊列中。  注意,這個時候,線程1 并沒有被喚醒。

6. signal方法執行完畢,線程2調用reentrantLock.unLock()方法,釋放鎖。這個時候因為AQS中隻有線程1,于是,AQS釋放鎖後按從頭到尾的順序喚醒線程時,線程1被喚醒,于是線程1回複執行。

7. 直到釋放所整個過程執行完畢。

可以看到,整個協作過程是靠結點在AQS的等待隊列和Condition的等待隊列中來回移動實作的,Condition作為一個條件類,很好的自己維護了一個等待信号的隊列,并在适時的時候将結點加入到AQS的等待隊列中來實作的喚醒操作。

看到這裡,signal方法的代碼應該不難了解了。

取出頭結點,然後doSignal

Java并發程式設計- Lock和condition的實作細節 AQS(推薦讀)

01

private

void

doSignal(Node first) {

02

do

{

03

if

( (firstWaiter = first.nextWaiter) == 

null

//修改頭結點,完成舊頭結點的移出工作

04

lastWaiter = 

null

;

05

first.nextWaiter = 

null

;

06

while

(!transferForSignal(first) &&

//将老的頭結點,加入到AQS的等待隊列中

07

(first = firstWaiter) != 

null

);

08

}

09

10

final

boolean

transferForSignal(Node node) {

11

14

if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))

15

return false;

16

17

23

Node p = enq(node);

24

int

ws = p.waitStatus;

25

//如果該結點的狀态為cancel 或者修改waitStatus失敗,則直接喚醒。

26

if

(ws > 

|| !compareAndSetWaitStatus(p, ws, Node.SIGNAL))

27

LockSupport.unpark(node.thread);

28

return

true

;

29

}

可以看到,正常情況 ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL) 這個判斷是不會為true的,是以,不會在這個時候喚醒該線程。

隻有到發送signal信号的線程調用reentrantLock.unlock()後因為它已經被加到AQS的等待隊列中,是以才會被喚醒。

總結:

     本文從代碼的角度說明了Condition的實作方式,其中,涉及到了AQS的很多操作,比如AQS的等待隊列實作獨占鎖功能,不過,這不是本文讨論的重點,等有機會再将AQS的實作單獨分享出來。