java.util.concurrent解析——AbstractQueuedSynchronizer綜述
盡管JVM在並發上已經做了很多優化工作,如偏向鎖、輕量級鎖、自旋鎖等等。但是基於Synchronized
wait
notify
實現的同步機製還是無法滿足日常開發中。原生同步機製在時間和空間上的開銷也一直備受詬病。為了提升Java程序在並發場景下的性能、擴展性和健壯性,java.util.concurrent的使用必不可少。java.util.concurrent 包含許多線程安全、測試良好、高性能的並發構建塊。通過使用java.util.concurrent,開發人員可以提高並發類的線程安全、可伸縮性、性能、可讀性和可靠性。
java.util.concurrent
的功能很強大,要想完整了解其全部細節也是很不容易的,需要多年的學習和實踐經驗。不過,通過深入其核心部分,可以快速了解其骨架和底層實現機製。那麼誰才是java.util.concurrent
的核心組件呢?稍微看過一點java.util.concurrent
源碼的同學知道,concurrent包下很多組件如:ReentrantLock
Semaphore
CountDownLatch
在其內部都有一個sync類,而這個sync
有繼承自java.util.concurrent.locks.AbstractQueuedSynchronizer
,而這個AbstractQueuedSynchronizer
就是concurrent包的核心。盡管AbstractQueuedSynchronizer
隻是一個類,但其實質上卻提供了一個框架,通過提供基於FIFO的隊列管理機製、線程阻塞機製和狀態同步機製,用戶可以快速基於AbstractQueuedSynchonizer
完成一係列複雜的進程同步操作。如果第一次接觸到AbstractQueuedSynchronizer
,建議讀一下其作者的論文:The java.util.concurrent Synchronizer Framework。
1 概述
AbstractQueuedSynchronizer
(以下簡稱AQS
)從字麵理解是一個抽象的基於隊列的同步器,所以AQS
至少要完成以下幾部分工作:
- 同步狀態的原子性管理
- 等待線程隊列的維護
- 線程的阻塞和喚醒
- 僅定義核心操作,留出足夠的擴展性給子類
AQS
定義了兩個核心操作:acquire
release
及其變種。前者用於進入同步塊前獲取同步塊執行權,後者用於釋放對於同步塊的占有權。
acquire
核心邏輯如下:
// 循環裏不斷嚐試,典型的失敗後重試
while (synchronization state does not allow acquire) {
// 同步狀態不允許獲取,進入循環體,也就是失敗後的處理
enqueue current thread if not already queued; // 如果當前線程不在等待隊列裏,則加入等待隊列
possibly block current thread; // 可能的話,阻塞當前線程
}
// 執行到這裏,說明已經成功獲取,如果之前有加入隊列,則出隊列。
dequeue current thread if it was queued;
release
核心邏輯如下:
update synchronization state; // 更新同步狀態
if (state may permit a blocked thread to acquire) // 檢查狀態是否允許一個阻塞線程獲取
unblock one or more queued threads; // 允許,則喚醒後繼的一個或多個阻塞線程。
而要實現上述兩個核心接口,就必須實現前文提到的AQS
主要工作的前三項:
- 同步狀態的原子性管理
- 阻塞線程隊列的維護
- 線程的阻塞和喚醒
實際使用中,AQS
提供了以下5個模板方法:
tryAcquire(int) // 試圖在獨占模式下獲取對象狀態。此方法應該查詢是否允許它在獨占模式下獲取對象狀態,如果允許,則獲取它。
tryRelease(int) // 試圖設置狀態來反映獨占模式下的一個釋放。
tryAcquireShared(int) // 試圖在共享模式下獲取對象狀態。此方法應該查詢是否允許它在共享模式下獲取對象狀態,如果允許,則獲取它。
tryReleaseShared(int) // 試圖設置狀態來反映共享模式下的一個釋放。
isHeldExclusively() // 如果對於當前(正調用的)線程,同步是以獨占方式進行的,則返回 true。此方法是在每次調用非等待 AbstractQueuedSynchronizer.ConditionObject 方法時調用的。(等待方法則調用 release(int)。)
2 實現
2.1 同步狀態的原子性管理
AQS
內部維護一個32bit字段state
用於描述當前狀態,state
字段有volatile
修飾,保證了其可見性。同時AQS
還提供了getState
,setState
, compareAndSetState
等方法用於狀態的讀取和更新:
- getState:提供一個基於內存語義(memory semantics)的volatile變量(state)讀取
- setState:提供一個基於內存予以(memory semantics)的volatile變量(state)更新
- compareAndSetState:提供一個基於CAS(compare and swap)的原子性狀態更新操作
通過簡單的原子讀寫就可以達到內存可視性,減少了同步的需求。子類可以獲取和設置狀態的值,通過定義狀態的值來表示 AQS 對象是否被獲取或被釋放。
2.2 線程的阻塞與喚醒
AQS
基於java.util.concurrent.locks.LockSupport
支持創建鎖和其他同步類需要的基本線程阻塞、解除阻塞原語。
這個類最主要的功能有兩個:
- park:把線程阻塞
- unpark:讓線程恢複執行
其實除了LockSupport
,Java之初就有Object
對象的wait和notify方法可以實現線程的阻塞和喚醒。那麼它們的區別是什麼呢?
主要的區別應該說是它們麵向的對象不同。阻塞和喚醒是對於線程來說的,LockSupport的park/unpark更符合這個語義,以“線程”作為方法的參數, 語義更清晰,使用起來也更方便。而wait/notify的實現使得“線程”的阻塞/喚醒對線程本身來說是被動的,要準確的控製哪個線程、什麼時候阻塞/喚醒很困難, 要不隨機喚醒一個線程(notify)要不喚醒所有的(notifyAll)。
LockSupport
並不需要獲取對象的監視器。LockSupport機製是每次unpark
給線程1個“許可”——最多隻能是1,而park
則相反,如果當前 線程有許可,那麼park方法會消耗1個並返回,否則會阻塞線程直到線程重新獲得許可,在線程啟動之前調用park/unpark
方法沒有任何效果。
// 1次unpark給線程1個許可
LockSupport.unpark(Thread.currentThread());
// 如果線程非阻塞重複調用沒有任何效果
LockSupport.unpark(Thread.currentThread());
// 消耗1個許可
LockSupport.park(Thread.currentThread());
// 阻塞
LockSupport.park(Thread.currentThread());
因為它們本身的實現機製不一樣,所以它們之間沒有交集,也就是說LockSupport阻塞的線程,notify/notifyAll沒法喚醒。
2.3 隊列維護
隊列管理是AQS
的核心部分,作者采用了基於CLH鎖隊列
來實現內部隊列。CLH鎖(可參考:CLH鎖)通常用於自旋鎖,我們反而用於阻塞同步器,但使用相同的基本策略:在(線程)它自己結點持有關於線程的一些控製信息。每個結點的 “status” 字段跟蹤一個線程是否應該阻塞。一個結點在它的前驅釋放時被通知。隊列的每個結點作為一個特定通知風格(specific-notification-style)的監視器服務,持有單一等待線程。”status” 字段不控製線程是否授予。一個線程可能嚐試去獲取如果它是第一個進入隊列,但成為第一個不保證就成功;它隻是獲得權利去競爭,所以當前釋放的競爭者線程可能需要再次等待(注:這是公平性的問題,子類的實現可以進行控製)。
為了進入CLH鎖隊列,你隻需要原子地把它作為一個新的尾結點拚接;為了出隊列,你隻需要設置 “head” 字段。
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
隊列部分比較複雜,詳細的介紹請參考下一篇博客。
3 總結
本文隻在於提綱挈領式地指出AQS
的大致框架以及主要作用,讀者需要了解作為一個維護內部競爭隊列的同步器,AQS
需要完成三部分工作:
- 共享狀態的原子性維護
- 線程的阻塞與喚醒
- 競爭隊列的維護
最後更新:2017-07-26 09:04:19