摘要:開始獲取鎖終于輪到出場了,的調用過程和完全一樣,同樣拿不到鎖,然后加入到等待隊列隊尾然后,在阻塞前需要把前驅結點的狀態置為,以確保將來可以被喚醒至此,的執行也暫告一段落了安心得在等待隊列中睡覺。
本文首發于一世流云的專欄:https://segmentfault.com/blog...一、本章概述
本章以ReentrantLock的調用為例,說明AbstractQueuedSynchronizer提供的獨占功能。
本章結構如下:
以ReentrantLock的公平策略為例,分析AbstractQueuedSynchronizer的獨占功能
以ReentrantLock的非公平策略為例,分析AbstractQueuedSynchronizer的獨占功能
分析AbstractQueuedSynchronizer的鎖中斷、限時等待等功能
二、ReentrantLock的公平策略原理本節對ReentrantLock公平策略的分析基于以下示例:
假設現在有3個線程:ThreadA、ThreadB、ThreadC,一個公平的獨占鎖,3個線程會依次嘗試去獲取鎖:ReentrantLock lock=new ReentrantLock(true);
線程的操作時序如下:
//ThreadA lock //ThreadB lock //ThreadC lock //ThreadA release //ThreadB release //ThreadC release2.1 ThreadA首先獲取到鎖
ThreadA首先調用ReentrantLock的lock方法,我們看下該方法的內部:
最終其實調用了FairSync的lock方法:
acquire方法來自AQS:
其中tryAcquire方法需要AQS的子類自己去實現,我們來看下ReentrantLock中的實現:
可以看到,在ReentrantLock中,同步狀態State的含義如下:
State | 資源的定義 |
---|---|
0 | 表示鎖可用 |
1 | 表示鎖被占用 |
大于1 | 表示鎖被占用,且值表示同一線程的重入次數 |
ThreadA是首個獲取鎖的線程,所以上述方法會返回true,第一階段結束。(ThreadA一直保持占有鎖的狀態)
此時,AQS中的等待隊列還是空:
終于,ThreadB要登場了,一樣,ThreadB先去調用lock方法,最終調用AQS的acquire方法:
tryAcquire方法肯定是返回false(因為此時ThreadA占有著鎖)。
接下來看下addWaiter方法,這個方法其實就是將當前調用線程包裝成一個【獨占結點】,添加到等待隊列尾部。
這里關鍵是enq方法,因為并發插入的情況存在,所以該方法設計成了自旋操作,保證結點能成功插入,具體步驟如下:
①當隊列為空的時候,先創建一個dummy頭結點;
②進入下一次循環,插入隊尾結點。
好了,ThreadB已經被包裝成結點插入隊尾了,接下來會調用acquireQueued方法,這也是AQS中最重要的方法之一:
在AQS中,等待隊列中的線程都是阻塞的,當某個線程被喚醒時,只有該線程是首結點(線程)時,才有權去嘗試獲取鎖。
上述方法中,將ThreadB包裝成結點插入隊尾后,先判斷ThreadB是否是首結點(注意不是頭結點,頭結點是個dummy結點),發現確實是首結點(node.predecessor==head),于是調用tryAcquire嘗試獲取鎖,但是獲取失敗了(此時ThreadA占有著鎖),就要判斷是否需要阻塞當前線程。
判斷是否需要阻塞線程:
注意,對于獨占功能,只使用了3種結點狀態:
結點狀態 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 取消。表示后驅結點被中斷或超時,需要移出隊列 |
SIGNAL | -1 | 發信號。表示后驅結點被阻塞了(當前結點在入隊后、阻塞前,應確保將其prev結點類型改為SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。) |
CONDITION | -2 | Condition專用。表示當前結點在Condition隊列中,因為等待某個條件而被阻塞了 |
對于在等待隊列中的線程,如果要阻塞它,需要確保將來有線程可以喚醒它,AQS中通過將前驅結點的狀態置為SIGNAL:-1來表示將來會喚醒當前線程,當前線程可以安心的阻塞。
看下圖或許比較好理解:
①插入完ThreadB后,隊列的初始狀態如下:
②雖然ThreadB是隊首結點,但是它拿不到鎖(被ThreadA占有著),所以ThreadB會阻塞,但在阻塞前需要設置下前驅的狀態,以便將來可以喚醒我:
至此,ThreadB的執行也暫告一段落了(安心得在等待隊列中睡覺)。
注意:補充一點,如果ThreadB在阻塞過程中被中斷,其實是不會拋出異常的,只會在acquireQueued方法返回時,告訴調用者在阻塞器件有沒被中斷過,具體如果處理,要不要拋出異常,取決于調用者,這其實是一種延時中斷機制。2.3 ThreadC開始獲取鎖
終于輪到ThreadC出場了,ThreadC的調用過程和ThreadB完全一樣,同樣拿不到鎖,然后加入到等待隊列隊尾:
然后,ThreadC在阻塞前需要把前驅結點的狀態置為SIGNAL:-1,以確保將來可以被喚醒:
至此,ThreadC的執行也暫告一段落了(安心得在等待隊列中睡覺)。
2.4 ThreadA釋放鎖ThreadA終于使用完了臨界資源,要釋放鎖了,來看下ReentrantLock的unlock方法:
unlock內部調用了AQS的release方法,傳參1:
嘗試釋放鎖的操作tryRelease:
釋放成功后,調用unparkSuccessor方法,喚醒隊列中的首結點:
此時,隊列狀態為:
好了,隊首結點(ThreadB)被喚醒了。
ThreadB會繼續從以下位置開始執行,先返回一個中斷標識,用于表示ThreadB在阻塞期間有沒被中斷過:
然后ThreadB又開始了自旋操作,被喚醒的是隊首結點,所以可以嘗試tryAcquire獲取鎖,此時獲取成功(ThreadA已經釋放了鎖)。
獲取成功后會調用setHead方法,將頭結點置為當前結點,并清除線程信息:
最終的隊列狀態如下:
ThreadB也終于使用完了臨界資源,要釋放鎖了,過程和ThreadA釋放時一樣,釋放成功后,會調用unparkSuccessor方法,喚醒隊列中的首結點:
隊首結點(ThreadC)被喚醒后,繼續從原來的阻塞處向下執行,并嘗試獲取鎖,獲取成功,最終隊列狀態如下:
ThreadC也終于使用完了臨界資源,要釋放鎖了。釋放成功后,調用unparkSuccessor方法,喚醒隊列中的首結點:
此時隊列中只剩下一個頭結點(dummy),所以這個方法其實什么都不做。最終隊列的狀態就是只有一個dummy頭結點。
至此,AQS的獨占功能已經差不多分析完了,剩下還有幾個內容沒分析:
鎖中斷功能
限時等待功能
Conditon等待功能
這些功能將在后續章節陸續分析。
三、ReentrantLock的非公平策略原理ReenrantLock非公平策略的內部實現和公平策略沒啥太大區別:
非公平策略和公平策略的最主要區別在于:
公平鎖獲取鎖時,會判斷等待隊列中是否有線程排在當前線程前面。只有沒有情況下,才去獲取鎖,這是公平的含義。
非公平鎖獲取鎖時,會立即嘗試修改同步狀態,失敗后再調用AQS的acquire方法。
acquire方法會轉調非公平鎖自身的tryAcquire方法,其實最終是調了nofairTryAcquire方法,而該方法相對于公平鎖,只是少了“隊列中是否有其它線程排在當前線程前”這一判斷:
還是以ReentrantLock為例,來看下AQS是如何實現鎖中斷和超時的。
我們知道ReentrantLock的lockInterruptibly方法是會響應中斷的。(線程如果在阻塞過程中被中斷,會拋出InterruptedException異常)
該方法調用了AQS的acquireInterruptibly方法:
上述代碼會先去嘗試獲取鎖,如果失敗,則調用doAcquireInterruptibly方法,如下:
很眼熟有木有?看下和acquireQueued方法的對比,唯一的區別就是:
當調用線程獲取鎖失敗,進入阻塞后,如果中途被中斷,acquireQueued只是用一個標識記錄線程被中斷過,而doAcquireInterruptibly則是直接拋出異常。
Lock接口中有一個方法:tryLock,用于在指定的時間內嘗試獲取鎖,獲取不到就返回。
ReentrantLock實現了該方法,可以看到,該方法內部調用了AQS的tryAcquireNanos方法:
tryAcquireNanos方法是響應中斷的,先嘗試獲取一次鎖,失敗則調用doAcquireNanos方法進行超時等待:
關鍵是doAcquireNano方法,和acquireQuqued方法類似,又是一個自旋操作,在超時前不斷嘗試獲取鎖,獲取不到則阻塞(加上了等待時間的判斷)。該方法內部,調用了LockSupport.parkNanos來超時阻塞線程:
LockSupport.parkNanos內部其實通過Unsafe這個類來操作線程的阻塞,底層是一個native方法:
如果當前線程在指定時間內獲取不到鎖,除了返回false外,最終還會執行cancelAcquire方法:
為了便于理解還是以3個線程為例:
假設現在有3個線程:ThreadA、ThreadB、ThreadC,一個公平的獨占鎖,3個線程會依次嘗試去獲取鎖,不過此時加上了限時等待:ThreadB等待10s,ThreadA等待20s。
ReentrantLock lock=new ReentrantLock(true); //ThreadA tryLock //ThreadB tryLock, 10s //ThreadC tryLock, 20s //ThreadA release //ThreadB release //ThreadC release
1. ThreadA首先獲取到鎖,ThreadB和ThreadC依次嘗試去獲取鎖
ThreadB和ThreadC經過兩輪自旋操作后,等待隊列的情況如下:
2. ThreadB先到超時時間
調用了cancelAcquire方法取消操作,隊列狀態變成:
3. ThreadC到達超時時間
調用了cancelAcquire方法取消操作,隊列狀態變成:
在退出cancelAcquire后,原來ThreadB和ThreadC對應的結點會被JVM垃圾回收器回收。
六、總結本章從ReentrantLock入手,分析AQS的獨占功能的內部實現細節。下一章,從CountDownLatch入手,看下AQS的共享功能如何實現。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76528.html
摘要:公平策略在多個線程爭用鎖的情況下,公平策略傾向于將訪問權授予等待時間最長的線程。使用方式的典型調用方式如下二類原理的源碼非常簡單,它通過內部類實現了框架,接口的實現僅僅是對的的簡單封裝,參見原理多線程進階七鎖框架獨占功能剖析 showImg(https://segmentfault.com/img/remote/1460000016012582); 本文首發于一世流云的專欄:https...
摘要:好了,繼續向下執行,嘗試獲取鎖失敗后,會調用首先通過方法,將包裝成共享結點,插入等待隊列,插入完成后隊列結構如下然后會進入自旋操作,先嘗試獲取一次鎖,顯然此時是獲取失敗的主線程還未調用,同步狀態還是。 showImg(https://segmentfault.com/img/remote/1460000016012541); 本文首發于一世流云的專欄:https://segmentfa...
摘要:關于,最后有兩點規律需要注意當的等待隊列隊首結點是共享結點,說明當前寫鎖被占用,當寫鎖釋放時,會以傳播的方式喚醒頭結點之后緊鄰的各個共享結點。當的等待隊列隊首結點是獨占結點,說明當前讀鎖被使用,當讀鎖釋放歸零后,會喚醒隊首的獨占結點。 showImg(https://segmentfault.com/img/remote/1460000016012293); 本文首發于一世流云的專欄:...
摘要:關于接口的介紹,可以參見多線程進階二鎖框架接口。最終線程釋放了鎖,并進入阻塞狀態。當線程被通知喚醒時,則是將條件隊列中的結點轉換成等待隊列中的結點,之后的處理就和獨占功能完全一樣。 showImg(https://segmentfault.com/img/remote/1460000016012490); 本文首發于一世流云的專欄:https://segmentfault.com/bl...
摘要:在時,引入了包,該包中的大多數同步器都是基于來構建的。框架提供了一套通用的機制來管理同步狀態阻塞喚醒線程管理等待隊列。指針用于在結點線程被取消時,讓當前結點的前驅直接指向當前結點的后驅完成出隊動作。 showImg(https://segmentfault.com/img/remote/1460000016012438); 本文首發于一世流云的專欄:https://segmentfau...
閱讀 2234·2021-11-17 09:33
閱讀 2774·2021-11-12 10:36
閱讀 3395·2021-09-27 13:47
閱讀 884·2021-09-22 15:10
閱讀 3485·2021-09-09 11:51
閱讀 1392·2021-08-25 09:38
閱讀 2757·2019-08-30 15:55
閱讀 2608·2019-08-30 15:53