ReentrantLock及AQS淺談

一、AQS簡介

        AQS全稱AbstractQueuedSynchronizer,是java併發包中的一個類,該類更像是一個框架,提供了一些模板方法供子類實現,從而實現了不同的同步器,如下圖所示。ReentrantLock,ReentrantReadWriteLock,ThreadPoolExecutor這些常見類都使用了AQS。
 
b16a060456c14ef3adecd11902e6e542.jpg?ima
 
以下是AQS的成員變量:
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
static final long spinForTimeoutThreshold = 1000L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;
 
       看到這裏大致能猜到AQS內部維護了一個雙向鏈表,head,tail分別指向頭尾,事實上,Node節點封裝了嘗試獲取鎖的線程對象,所有嘗試獲取鎖的線程組成了一個鏈表,在公平鎖情況下,例如ReentrantLock中的AQS子類FairSync,每次都是按照順序頭部節點先被喚醒並嘗試獲取鎖。
        state 是同步狀態位,具體是否能夠獲取鎖就是通過修改state來實現,下面會有具體代碼分析。
        spinForTimeoutThreshold相當於一個閾值,在一些提供等待時間的獲取鎖操作時,例如ReentrantLock. tryLock(long timeout, TimeUnit unit)方法,在判定是否需要阻塞線程時,如果時間小於spinForTimeoutThreshold,則不會被阻塞,用於快速響應一些等待時間很短的獲取鎖操作。
       其他成員變量是關於CAS操作的,AQS的很多操作都是基於CAS原子操作的,以確保線程安全。


 

二、ReentrantLock簡介


ReentrantLock是根據AQS實現的獨佔鎖,提供了兩個構造方法,如下圖所示:
 
e882a6c9c1ab48f18f5584aacd03745d.jpg?ima
 
ReentrantLock有三個內部類:Sync,NonfairSync,FairSync,繼承關係如下:
 
cf0a7dd24e404ed48f1d390383185656.jpg?ima
 
ReentrantLock提供兩種類型的鎖:公平鎖,非公平鎖。分別對應FairSync,NonfairSync。默認實現是NonFairSync。
ReentrantLock提供了lock(),lockInterruptibly(),tryLock(),tryLock(long timeout, TimeUnit unit)四種獲取鎖的方式。


 

三、非公平鎖lock源碼分析


下面從ReentrantLock.lock()簡述一下其源碼實現
 
d3a05a5398264c57876530b2028722d6.jpg?ima
 
lock()方法內部是委託給sync變量來實現的,下面是NonfairSync的lock方法源碼:
 
d70d3ec3bd3b475299b88a0fedd5195e.jpg?ima
 
       在NonfairSync的lock方法體裏,首先嚐試修改state的值,上面說到,AQS很多操作都是基於CAS的,在這裏,設置state的期望值是0(沒有線程持有鎖時的狀態),修改值爲1,如果成功,則返回true,並且設置持有鎖的線程爲當前線程。樂觀情況下,lock方法獲取鎖操作到這裏就結束了。
      但是,很多情況下並不是那麼樂觀,如果compareAndSetState操作失敗,就會進入到acquire方法:
 
590cbc14b3b64e6699a41b077e86ce14.jpg?ima
 
acquire方法在AQS類,這裏,首先會調用tryAcquire方法,該方法的具體實現在NonfairSync中:
0fdd002b317d4f1d94ebfe1253f60c7b.jpg?ima
 
tryAcquire方法內部調用了Sync的nonfairTryAcquire方法:
56f9ca4ea46e485f899a97eb836ffb4b.jpg?ima
 
       在Sync的nonfairTryAcquire方法體裏,如果state爲0,會再做一次compare and set操作,嘗試修改state的值爲1。
       如果state不爲0,判斷當前線程是否是持有獨佔鎖的線程,如果是,將state值加上acquires(傳入的是1),這裏就是ReentrantLock可重入的內部實現。
       如果方法返回true,那麼獲取鎖的操作結束,如果返回false,回到AQS的acquire()方法內部:
b32fe2414e964990b418c9e4bc1ca25c.jpg?ima
 
會繼續調用方法addWaiter,返回結果後,執行 acquireQueued方法。下面看一下addWaiter方法:
f89560bd5bbb463cb41c7b03590c0df0.jpg?ima
 
       如上圖所示,addWaiter方法的功能是將當前線程封裝成Node,並加入到AQS鏈表尾部,參數mode是傳入的Node.EXCLUSIVE,代表實例化的node是獨佔模式,而非共享模式,注意如果pred == null表示內部隊列還沒有初始化,則會調用enq(node)。或者pred != null 但是在compareAndSetTail失敗時,也會調用enq(node)。例如,同時有多個線程node嘗試加入到鏈表末尾,就會存在失敗的可能。
進入到enq方法內部:
c6e75b482ea745169542356ccd1fac03.jpg?ima
 
       這個方法的最外層是一個大的for循環,並且是一個死循環。出口返回條件只有一個:成功加入到鏈表的末尾。前面講到,在鏈表爲空或者添加node到鏈表末尾失敗時會進入到enq方法,這裏首先判斷tail是否爲空,如果爲空,實例化一個空的Node節點,並且tail和head都指向這個空的Node,如果不爲空,將node加入到鏈表末尾,如下圖所示:
 
f173188c27f646cd83194a39111e1165.jpg?ima
 
將node成功加入到鏈表中後,回到AQS的acquire()方法內部,開始執行acquireQueued方法:
 
221f2ba17d314ba39e99d1716efc4c4f.jpg?ima
 
       這裏也是一個循環,循環體內首先獲取node的前一個節點,即node.prev指向的節點p,接着判斷p是否是head節點
,如果是head節點,會嘗試獲取鎖,tryAcquire方法在上面已經分析過。獲取鎖成功之後,sethead(node)會把node節點置爲頭節點,p.next = null將之前的head節點指向斷掉,幫助jvm觸發GC。最後返回當前線程在獲取鎖過程中是否曾經被中斷。
       如果node.prev不是頭節點,不會嘗試獲取鎖,這也就是AQS內部鏈表的作用,會從鏈表的頭部開始嘗試獲取鎖,達到一個FIFO的作用。獲取鎖失敗或者node.prev不是頭節點,則會執行shouldParkAfterFailedAcquire:
f15bfc353bd64831bee85d3b7893ecbb.jpg?ima
 
       Node.SIGNAL說明該節點準備好被喚醒,若節點沒有設置爲該狀態,線程不會阻塞。
shouldParkAfterFailedAcquire方法有三個作用:1、若pred.waitStatus狀態位大於0,說明這個節點已經取消了獲取鎖的操作,doWhile循環會遞歸刪除掉這些放棄獲取鎖的節點。2、若狀態位不爲Node.SIGNAL,且沒有取消操作,則會嘗試將狀態位修改爲Node.SIGNAL。3、狀態位是Node.SIGNAL,表明線程是否已經準備好被阻塞並等待喚醒。
       最終,只有在pred.waitStatus已經等於Node.SIGNAL時纔會返回true。其他情況返回false,然後acquireQueued會繼續循環。
       在shouldParkAfterFailedAcquire返回true之後,acquireQueued方法體內繼續執行parkAndCheckInterrupt():
 
fd689658103747a886c3d6ce2c0eba36.jpg?ima

       該方法調用LockSupport.park()方法使線程阻塞。注意,ReentrantLock.lock()獲取鎖阻塞就是在這一步實現。阻塞的線程在其他線程釋放鎖之後會被LockSupport.unpark()喚醒。LockSupport.park(),LockSuppoert.unpark()最終都是調用了UNSAFE的native方法,這裏不做分析。整個ReentrantLock.lock方法就分析到這裏,下面看一下unlock操作。

 


四、非公平鎖unlock源碼分析


ReentrantLock.unlock()方法內部同樣是交給sync的release實現:
 
04fb606daa134613989bfc81302beeac.jpg?ima
 
sync.release()方法調用是在父類AQS中, release方法會先調用子類Sync的tryRelease()方法,如下圖所示:
 
663b1b4085814040a64b2cee3f542a75.jpg?ima
 
如下是子類Sync.tryRelease()的源碼:
 
74ece6f490034d2e897eb0e2f448c3a4.jpg?ima
 
       首先獲取state值,並減去releases,這裏releases爲1。若當前線程非獨佔鎖擁有線程,拋出異常。若減去1後state爲0,說明可以喚醒其他線程嘗試獲取鎖,將free設置爲true並返回,設置獨佔鎖擁有者爲null。
       如果不爲0,設置state爲減少後的值並且返回false,這樣的話,就不會有後面喚醒其他線程的操作。所以,需要注意可重入的鎖,在獲取鎖的時候,調用了多少次lock方法,釋放鎖時,就需要調用多少次unlock方法。
       在返回值爲true之後,回到父類的release方法,最終會調用unparkSuccessor()方法:
32b7a3adf32a4cc58459c0385dc961e8.jpg?ima
 
       在unparkSuccessor方法中,會獲取node.next,使變量s = node.next。若s不爲空且狀態位小於0,則滿足喚醒條件,執行LockSupport.unpark()喚醒線程。否則執行for循環遞歸查找到離head最近的一個待喚醒節點喚醒,節點喚醒之後會繼續執行獲取鎖的操作,上面已經做過分析,這裏不做贅述。


 

五、其他


       由於篇幅原因,只討論了非公平鎖的實現,在這裏大概講一下公平鎖的“公平”體現在哪裏,根據上面講到的,非公平鎖獲取鎖有兩個地方:
1、在NonfairSync.lock方法體入口處就直接獲取鎖然後退出方法;
2、加入到鏈表中,每次鏈表頭部的節點被喚醒,接着嘗試獲取鎖。雖然頭部節點被喚醒之後,會嘗試獲取鎖,但可能會有線程在在NonfairSync.lock方法體入口處不進入鏈表就直接取得了鎖。
       而在公平鎖中,如下圖所示,hasQueuedPredecessors會首先判斷鏈表中是否有排隊線程,沒有排隊線程纔會嘗試獲取鎖,否則加入到鏈表排隊。嚴格保證了所有線程都是按照鏈表順序先入先出的獲取鎖。
 
a86e60fcde5b4d8a95848c41c42a5e8b.jpg?ima



 網易雲捕-高效的APP質量跟蹤平臺

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