AbstractQueuedSynchronizer 超詳細原理解析
今天我們來學習一下 AbstractQueuedSynchronizer
類的相關原理, java的concurrent
包中很多類都依賴於這個類,比如說常用的 ReentranLock
, Semaphore
和 CountDownLatch
等。 為了方便理解,我們以一段使用 ReentranLock
的程式碼為例,講解 ReentranLock
每個方法中有關 AQS
的使用。
ReentranLock示例
我們都知道 ReentranLock
的加鎖行為和 Synchronized
類似,都是可重入的鎖,但是二者的實現方式確實完全不同的,我們之後也會講解 Synchronized
的原理。 除此之外,Synchronized的阻塞無法被中斷,而ReentrantLock則提供了可中斷的阻塞 。下面的程式碼是 ReentranLock
的函式,我們就以此為順序,依次講解這些函式背後的實現原理。
公平鎖和非公平鎖
ReentranLock
分為公平鎖和非公平鎖,二者的區別就在獲取鎖機會是否和排隊順序相關。我們都知道,如果鎖被另一個執行緒持有,那麼申請鎖的其他執行緒會被掛起等待,加入等待佇列。
理論上,先呼叫 lock
函式被掛起等待的執行緒應該排在等待佇列的前端,後呼叫的就排在後邊。如果此時,鎖被釋放,需要通知等待執行緒再次嘗試獲取鎖,公平鎖會讓最先進入佇列的執行緒獲得鎖。而非公平鎖則會喚醒所有執行緒,讓它們再次嘗試獲取鎖,所以可能會導致後來的執行緒先獲得了鎖,則就是非公平。
我們會發現 FairSync
和 NonfairSync
都繼承了 Sync
類,而 Sync
的父類就是 AbstractQueuedSynchronizer
(後續簡稱 AQS
)。但是 AQS
的建構函式是空的,並沒有任何操作。 之後的原始碼分析,如果沒有特別說明,就是指公平鎖。
lock操作
ReentranLock
的 lock
函式如下所示,直接呼叫了 sync
的 lock
函式。也就是呼叫了 FairSync
的 lock
函式。
我們接下來就正式開始 AQS
相關的原始碼分析了, acquire
函式的作用是獲取同一時間段內只能被一個執行緒獲取的量,這個量就是抽象化的鎖概念。我們先分析程式碼,你慢慢就會明白其中的含義。
tryAcquire
, addWaiter
和 acquireQueued
都是十分重要的函式,我們接下來依次學習一下這些函式,理解它們的作用。
由上述程式碼我們可以發現, tryAcquire
就是嘗試獲取那個執行緒獨佔的變數 state
。state的值表示其狀態:如果是0,那麼當前還沒有執行緒獨佔此變數;否在就是已經有執行緒獨佔了這個變數,也就是代表已經有執行緒獲得了鎖。但是這個時候要再進行一次判斷,看是否是當前執行緒自己獲得的這個鎖,如果是,就增加state的值。
這裡有幾點需要說明一下,首先是 compareAndSetState
函式,這是使用CAS操作來設定 state
的值,而且state值設定了 volatile
修飾符,通過這兩點來確保修改state的值不會出現多執行緒問題。然後是公平鎖和非公平鎖的區別問題,在 UnfairSync
的 nonfairTryAcquire
函式中不會在相同的位置上呼叫 hasQueuedPredecessors
來判斷當前是否已經有執行緒在排隊等待獲得鎖。
如果 tryAcquire
返回 true
,那麼就是獲取鎖成功;如果返回false,那麼就是未獲得鎖,需要加入阻塞等待佇列。我們下面就來看一下 addWaiter
的相關操作。
等待鎖的阻塞佇列
將儲存當前執行緒資訊的節點加入到等待佇列的相關函式中涉及到了無鎖佇列的相關演算法,由於在 AQS
中只是將節點新增到隊尾,使用到的無鎖演算法也相對簡單。真正的無鎖佇列的演算法我們等到分析 ConcurrentSkippedListMap
時在進行講解。
通過呼叫 addWaiter
函式, AQS
將當前執行緒加入到了等待佇列,但是還沒有阻塞當前執行緒的執行,接下來我們就來分析一下 acquireQueued
函式。
等待佇列節點的操作
由於進入阻塞狀態的操作會降低執行效率,所以, AQS
會盡力避免試圖獲取獨佔性變數的執行緒進入阻塞狀態。所以,當執行緒加入等待佇列之後, acquireQueued
會執行一個for迴圈,每次都判斷當前節點是否應該獲得這個變數(在隊首了)。如果不應該獲取或在再次嘗試獲取失敗,那麼就呼叫 shouldParkAfterFailedAcquire
判斷是否應該進入阻塞狀態。如果當前節點之前的節點已經進入阻塞狀態了,那麼就可以判定當前節點不可能獲取到鎖,為了防止CPU不停的執行for迴圈,消耗CPU資源,呼叫 parkAndCheckInterrupt
函式來進入阻塞狀態。
阻塞和中斷
由上述分析,我們知道了 AQS
通過呼叫 LockSupport
的 park
方法來執行阻塞當前程序的操作。其實,這裡的阻塞就是執行緒不再執行的含義,通過呼叫這個函式,執行緒進入阻塞狀態,上述的 lock
操作也就阻塞了,等待中斷或在獨佔性變數被釋放。
關於中斷的相關知識,我們以後再說,就繼續沿著 AQS
的主線,看一下釋放獨佔性變數的相關操作吧。
unlock操作
與 lock
操作類似, unlock
操作呼叫了 AQS
的 relase
方法,引數和呼叫 acquire
時一樣,都是1。
由上述程式碼可知,release就是先呼叫 tryRelease
來釋放獨佔性變數。如果成功,那麼就看一下是否有等待鎖的阻塞執行緒,如果有,就呼叫 unparkSuccessor
來喚醒他們。
我們可以看到 tryRelease
中的邏輯也體現了可重入鎖的概念,只有等到 state
的值為0時,才代表鎖真正被釋放了。所以獨佔性變數 state
的值就代表鎖的有無。當 state=0
時,表示鎖未被佔有,否在表示當前鎖已經被佔有。
呼叫了 unpark
方法後,進行 lock
操作被阻塞的執行緒就恢復到執行狀態,就會再次執行 acquireQueued
中的無限for迴圈中的操作,再次嘗試獲取鎖。
後記
有關 AQS
和 ReentrantLock
的分析就差不多結束了。不得不說,我第一次看到AQS的實現時真是震驚,以前都認為 Synchronized
和 ReentrantLock
的實現原理是一致的,都是依靠java虛擬機器的功能實現的。沒有想到還有 AQS
這樣一個背後大Boss在提供幫助啊。學習了這個類的原理,我們對JUC的很多類的分析就簡單了很多。此外, AQS
涉及的 CAS
操作和無鎖佇列的演算法也為我們學習其他無鎖演算法提供了基礎。 知識的海洋是無限的啊!