一、AQS簡介
AQS全稱AbstractQueuedSynchronizer,是java併發包中的一個類,該類更像是一個框架,提供了一些模板方法供子類實現,從而實現了不同的同步器,如下圖所示。ReentrantLock,ReentrantReadWriteLock,ThreadPoolExecutor這些常見類都使用了AQS。
以下是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實現的獨佔鎖,提供了兩個構造方法,如下圖所示:
ReentrantLock有三個內部類:Sync,NonfairSync,FairSync,繼承關係如下:
ReentrantLock提供兩種類型的鎖:公平鎖,非公平鎖。分別對應FairSync,NonfairSync。默認實現是NonFairSync。
ReentrantLock提供了lock(),lockInterruptibly(),tryLock(),tryLock(long timeout, TimeUnit unit)四種獲取鎖的方式。
三、非公平鎖lock源碼分析
下面從ReentrantLock.lock()簡述一下其源碼實現
lock()方法內部是委託給sync變量來實現的,下面是NonfairSync的lock方法源碼:
在NonfairSync的lock方法體裏,首先嚐試修改state的值,上面說到,AQS很多操作都是基於CAS的,在這裏,設置state的期望值是0(沒有線程持有鎖時的狀態),修改值爲1,如果成功,則返回true,並且設置持有鎖的線程爲當前線程。樂觀情況下,lock方法獲取鎖操作到這裏就結束了。
但是,很多情況下並不是那麼樂觀,如果compareAndSetState操作失敗,就會進入到acquire方法:
acquire方法在AQS類,這裏,首先會調用tryAcquire方法,該方法的具體實現在NonfairSync中:
tryAcquire方法內部調用了Sync的nonfairTryAcquire方法:
在Sync的nonfairTryAcquire方法體裏,如果state爲0,會再做一次compare and set操作,嘗試修改state的值爲1。
如果state不爲0,判斷當前線程是否是持有獨佔鎖的線程,如果是,將state值加上acquires(傳入的是1),這裏就是ReentrantLock可重入的內部實現。
如果方法返回true,那麼獲取鎖的操作結束,如果返回false,回到AQS的acquire()方法內部:
會繼續調用方法addWaiter,返回結果後,執行 acquireQueued方法。下面看一下addWaiter方法:
如上圖所示,addWaiter方法的功能是將當前線程封裝成Node,並加入到AQS鏈表尾部,參數mode是傳入的Node.EXCLUSIVE,代表實例化的node是獨佔模式,而非共享模式,注意如果pred == null表示內部隊列還沒有初始化,則會調用enq(node)。或者pred != null 但是在compareAndSetTail失敗時,也會調用enq(node)。例如,同時有多個線程node嘗試加入到鏈表末尾,就會存在失敗的可能。
進入到enq方法內部:
這個方法的最外層是一個大的for循環,並且是一個死循環。出口返回條件只有一個:成功加入到鏈表的末尾。前面講到,在鏈表爲空或者添加node到鏈表末尾失敗時會進入到enq方法,這裏首先判斷tail是否爲空,如果爲空,實例化一個空的Node節點,並且tail和head都指向這個空的Node,如果不爲空,將node加入到鏈表末尾,如下圖所示:
將node成功加入到鏈表中後,回到AQS的acquire()方法內部,開始執行acquireQueued方法:
這裏也是一個循環,循環體內首先獲取node的前一個節點,即node.prev指向的節點p,接着判斷p是否是head節點,如果是head節點,會嘗試獲取鎖,tryAcquire方法在上面已經分析過。獲取鎖成功之後,sethead(node)會把node節點置爲頭節點,p.next = null將之前的head節點指向斷掉,幫助jvm觸發GC。最後返回當前線程在獲取鎖過程中是否曾經被中斷。
如果node.prev不是頭節點,不會嘗試獲取鎖,這也就是AQS內部鏈表的作用,會從鏈表的頭部開始嘗試獲取鎖,達到一個FIFO的作用。獲取鎖失敗或者node.prev不是頭節點,則會執行shouldParkAfterFailedAcquire:
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():
該方法調用LockSupport.park()方法使線程阻塞。注意,ReentrantLock.lock()獲取鎖阻塞就是在這一步實現。阻塞的線程在其他線程釋放鎖之後會被LockSupport.unpark()喚醒。LockSupport.park(),LockSuppoert.unpark()最終都是調用了UNSAFE的native方法,這裏不做分析。整個ReentrantLock.lock方法就分析到這裏,下面看一下unlock操作。
四、非公平鎖unlock源碼分析
ReentrantLock.unlock()方法內部同樣是交給sync的release實現:
sync.release()方法調用是在父類AQS中, release方法會先調用子類Sync的tryRelease()方法,如下圖所示:
如下是子類Sync.tryRelease()的源碼:
首先獲取state值,並減去releases,這裏releases爲1。若當前線程非獨佔鎖擁有線程,拋出異常。若減去1後state爲0,說明可以喚醒其他線程嘗試獲取鎖,將free設置爲true並返回,設置獨佔鎖擁有者爲null。
如果不爲0,設置state爲減少後的值並且返回false,這樣的話,就不會有後面喚醒其他線程的操作。所以,需要注意可重入的鎖,在獲取鎖的時候,調用了多少次lock方法,釋放鎖時,就需要調用多少次unlock方法。
在返回值爲true之後,回到父類的release方法,最終會調用unparkSuccessor()方法:
在unparkSuccessor方法中,會獲取node.next,使變量s = node.next。若s不爲空且狀態位小於0,則滿足喚醒條件,執行LockSupport.unpark()喚醒線程。否則執行for循環遞歸查找到離head最近的一個待喚醒節點喚醒,節點喚醒之後會繼續執行獲取鎖的操作,上面已經做過分析,這裏不做贅述。
五、其他
由於篇幅原因,只討論了非公平鎖的實現,在這裏大概講一下公平鎖的“公平”體現在哪裏,根據上面講到的,非公平鎖獲取鎖有兩個地方:
1、在NonfairSync.lock方法體入口處就直接獲取鎖然後退出方法;
2、加入到鏈表中,每次鏈表頭部的節點被喚醒,接着嘗試獲取鎖。雖然頭部節點被喚醒之後,會嘗試獲取鎖,但可能會有線程在在NonfairSync.lock方法體入口處不進入鏈表就直接取得了鎖。
而在公平鎖中,如下圖所示,hasQueuedPredecessors會首先判斷鏈表中是否有排隊線程,沒有排隊線程纔會嘗試獲取鎖,否則加入到鏈表排隊。嚴格保證了所有線程都是按照鏈表順序先入先出的獲取鎖。
網易雲捕-高效的APP質量跟蹤平臺