1. 程式人生 > 實用技巧 >ReentrantLock AQS原始碼

ReentrantLock AQS原始碼

前言

    之前講過synchronized關鍵字在JDK1.7之前是一把重量級的鎖,那時JVM還未對synchronized關鍵字進行優化,所以synchronized會呼叫作業系統的函式實現加鎖和解鎖。而在JDK1.7後JVM對其進行優化,synchronized可以通過自旋達到一把輕量級的鎖,在JVM級別就可以得到實現,無須再呼叫作業系統函式。而在此期間併發大神Doug Lea就寫出了很多的併發工具類,ReentrantLock就是其中的一種,ReentrantLock所實現的功能也要比synchronized豐富,我們都知道ReentrantLock的實現底層還是依賴於AQS(Abstract Queued Synchronizer)同步佇列器實現,下面主要分析下ReentrantLock的加鎖和解鎖過程


ReentrantLock,Synchronized功能區別

    ReentrantLock支援lock()加鎖,支援超時加鎖,支援公平和非公平鎖,支援lockInterruptibly執行緒中斷響應,Synchronized關鍵字所能支援的功能就比較單一,Synchronized的好處就是不需要我們手動的加鎖和釋放鎖,JVM幫我們實現了。而ReentrantLock就需要我們手動的加鎖和釋放鎖,如果一旦鎖沒有得到釋放,那麼所造成的影響就比較大了,ReentrantLock和Synchronized都是可重入鎖


ReentrantLock所使用到的重要技術點

1. 自旋(for死迴圈獲取鎖)
2. CAS操作(通過CAS操作更新state鎖狀態,更新成功表示鎖獲取成功)
3. 依賴LockSupport.park()執行緒等待, LockSupport.unpark執行緒喚醒

AQS(AbstractQueuedSynchronizer)類實現的主要程式碼

private transient volatile Node head; //佇列的頭部節點
private transient volatile Node tail; //佇列的尾部節點
private volatile int state; //鎖的狀態,state=0表示自由狀態,沒有被執行緒佔用,state > 0表示被執行緒已經佔用

    佇列節點Node的主要實現程式碼

volatile Node prev; //上一個節點
volatile Node next; //下一個節點
volatile Thread thread; //節點中的執行緒
volatile int waitStatus; //表示下一個節點的狀態,是取消還是睡眠

    AQS中的佇列就有點類似於圖中, head頭部指向的節點的thread是null,隊首的節點不參與排隊競爭鎖資源


ReentrantLock公平和非公平加鎖區別

    公平鎖加鎖流程

    重點分析下tryAcquire方法獲取鎖

    我們可以看到執行緒去獲取鎖的時候會去判斷當前執行緒是否需要進行排隊操作,而後續繼續分析獲取鎖失敗(CAS操作失敗)怎樣去入隊的


    非公平鎖加鎖流程

    我們可以看到非公平鎖加鎖最後會呼叫到nonfairTryAcquire方法,而nonfairTryAcquire方法跟公平鎖的tryAcquire方法有點類似,只不過nonfairTryAcquire方法少了hasQueuedPredecessors方法,判斷是否需要入隊,因為是非公平的,沒有排隊的概念。

    以上就是公平鎖和非公平鎖加鎖的流程,下面來分析加鎖失敗ReentrantLock是怎樣去處理的,公平鎖和非公平鎖加鎖失敗後,入佇列的流程都是一樣的,都是依賴於AQS去實現的


AQS加鎖失敗,執行緒入隊流程

    前面我們分析到tryAcquire方法加鎖失敗,返回false,取反則是true。也就是會執行到&&後面的一個條件acquireQueued(addWaiter(Node.EXCLUSIVE), arg),這裡首先分析一下addWaiter方法,執行緒入隊操作

    來看一下addWaiter方法的處理流程

    首先會將當前執行緒thread初始化成一個Node,然後判斷尾部節點是否為空,如果為空,則直接執行enq方法,如果不能空,則改變尾部節點和當前節點Node的prev和next指標,通過compareAndSetTail操作將Node節點新增到尾部,這裡可能會出現CAS操作失敗,如果失敗,則也要執行enq方法,如果新增尾部成功,則直接返回,這裡要注意一個點addWaiter方法和後面要分析的enq方法都是先設定Node.prev指標。體現在後面unlock()釋放鎖喚醒佇列執行緒的時候是從隊尾向隊首找waitStatus<=0的節點,如果是通過next指標從隊首往隊尾去找waitStatus<=0的節點,就有可能會出現next指標還未進行設定,找不到下一個節點


    enq方法處理流程

    我們可以看到enq方法的流程是通過for死迴圈+CAS操作完成設定尾部節點,首先會檢測AQS佇列是否進行初始化,如果未進行初始化,則會初始化一個Node, Node中的thread=null的節點,head和tail指標一起指向Node,初始化完成之後再進行一次迴圈則會通過改變尾部節點和當前節點Node的prev和next指標,通過compareAndSetTail操作將Node節點新增到尾部,直到CAS操作成功返回

    以上addWaiter和enq方法就是入隊操作,邏輯還是比較清晰的,入完佇列之後,要開始進行LockSupport.park操作,看ReentrantLock是怎麼進行park操作的,下面看一下acquireQueued處理流程


AQS的acquireQueued流程

    前面分析到入隊完成,再執行acquireQueued方法,看一下acquireQueued的處理流程

    可以發現acquireQueued中也是有個for(;;)死迴圈來一直獲取鎖和park操作,還會判斷自己的上一個節點是否是頭節點,如果是頭節點的話,會再次嘗試去獲取鎖。如果還是獲取鎖失敗,則會判斷自己是否需要進行park操作,也就是判斷上一個節點的waitStatus==-1是否成立,如果成立,則會呼叫LockSupport.park()進行等待

    至此,ReentrantLock的整個加鎖流程就分析完成了,可以總結出ReentrantLock的加鎖使用到大量的自旋,CAS操作,最後進行park操作。最後流程圖我們看到還有一行程式碼Thread.interrupted(),不知道起到什麼作用?我們前面講到ReentrantLock還支援方法lockInterruptibly執行緒中斷響應


ReentrantLock支援的lockInterruptibly方法

    我們可以看到lockInterruptibly呼叫的acquireInterruptibly方法,會首先判斷一下Thread.interrupted()執行緒的中斷狀態,如果設定了中斷標識,則會丟擲異常,我們可以try catch異常,拿到響應。

    我們發現acquireInterruptibly再呼叫的doAcquireInterruptibly方法跟我們所分析的acquireQueued有點類似,後面同樣呼叫了parkAndCheckInterrupt方法,這裡發現Thread.interrupted()是中斷狀態的話,doAcquireInterruptibly會丟擲異常,來響應中斷。而acquireQueued方法不會丟擲異常而是將interrupted設定為true,最後返回的是interrupted

    而整個acquireQueued返回true時,就會執行selfInterrupt方法,將執行緒至為Thread.currentThread().interrupt()中斷狀態,這裡其實就是為了還原使用者行為,因為使用者將執行緒設定為interrupt狀態,而acquireQueued跟doAcquireInterruptibly方法呼叫了同樣的parkAndCheckInterrupt方法,又會將thread的interrupt狀態反置回來,所以後面又呼叫了selfInterrupt方法,將使用者的行為還原回來,這裡可能是大神Doug Lea不想重新再寫一個方法,所以讓他們兩者進行了複用哈哈哈