閱讀330 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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

  上一篇:go  java.util.concurrent解析——AbstractQueuedSynchronizer隊列管理
  下一篇:go  OSS提供的安全防護功能介紹