1. 程式人生 > 程式設計 >AbstractQueuedSynchronizer(aqs) JUC同步框架

AbstractQueuedSynchronizer(aqs) JUC同步框架

作者: doug lea
原文: gee.cs.oswego.edu/dl/papers/a…
參考: www.jianshu.com/p/c5a18eb6d…
相關好文: www.sohu.com/a/232590588… 鎖不慢;鎖競爭慢

文中會出現一些 "[number]"的標記,對應了原文中doug lea的參考文獻引用的編號

摘要

J2SE1.5 java.util中的大多數同步器(鎖、屏障等),基於輕量級框架構 AbstractQueuedSynchronizer 
該框架為原子管理同步狀態,阻塞喚醒執行緒,以及排隊提供了通用機制。
本文介紹了該框架的原理、設計、實現、使用和效能。
複製程式碼

類別和主題描述符

D.1.3 [Programming Techniques]: Concurrent Programming – Parallel Programming
D.1.3 [程式設計技術]: 併發程式設計 - 並行程式設計
複製程式碼

關鍵詞

同步(Synchronization),Java
複製程式碼

1.前言

java 發行版 J2SE-1.5 引入包 java.util.concurrent,包括一組通過 JCP(Java Community Process) 
JSR(Java Specification Request) 166 建立的中級併發支援類.
複製程式碼
    在這些元件中有一組同步器——抽象資料型別(ADT)類,來維護內部同步狀態(例如:表示lock/unlock),更新和檢查狀態,若狀態需要阻塞執行緒,再通過其他執行緒狀態的改變喚醒執行緒.
元件示例: 各種形式的互斥鎖(exclusion locks),讀寫鎖(read
-write locks),訊號量(semaphores),屏障(barriers),事件指示器(event indicators),交接佇列(handoff queues,即SynchronizedQueue) 複製程式碼

ADT: 此處的ADT指的是 abstract class AbstractQueuedSynchronizer //抽象類

眾所周知(如[2]),同步器之間幾乎可以互相實現. 例如用可重入鎖實現訊號量,反之亦然. 然而,
這樣做通常需要足夠的的複雜性、開銷和靈活性,不是最好解決方案. 此外這個方案在概念上沒有
吸引力. 如果這些結構沒有比其他結構更原始,開發人員不應該選擇其中一個作為構建其他元件的
基礎. 於是,JSR 166 建立了一個以 "AbstractQueuedSynchronizer(aqs)"
為中心的類庫,aqs 提供了大多數 "synchronizers" 公用的機制,"synchronizers" 只需補充實現各自的特性即可。 複製程式碼
本文的其餘部分將討論此框架的需求、設計和實現背後的主要思想、示例用法以及顯示其效能
特徵的一些測試
複製程式碼

2.需求

2.1 功能

同步器有兩類方法[7]: 
    "acquire" 方法阻塞呼叫執行緒,除非/直到同步狀態允許它繼續
    "release" 操作改變同步狀態,喚醒一條或多條執行緒.
    
JUC 沒有為同步器定義一個統一的API. 有些使用公用介面(Lock),其他的使用其專門的版本.
所以 acquire release,在不同類的不同方法中呼叫. 例如,Lock.lock,Semaphore.acquire,CountDownLatch.await,FutureTask.get 都使用框架中的 acquire 操作.
複製程式碼

FutureTask在 jdk1.8 的程式碼沒有使用acquire,release

必要情況下,框架對同步器提供的一些支援:
    非阻塞同步嘗試(tryLock) 和 阻塞版本
    可選的超時時間: tryAcquireNanos(time)
    響應執行緒中斷: acquire acquireInterruptibly

獨享模式,共享模式。

JUC定義了一個介面"Condition",支援監視器風格的 await/signal 操作.
複製程式碼

獨享模式: 同時只能有一個執行緒獲得鎖
共享模式: 同時有多條執行緒獲得鎖 監視器: java中每個物件都有一個監視器,配合synchronize使用 synchronize(obj){}

2.2 效能目標

    Java內建鎖(使用synchronized method和 synchronized blocks)長期以來一直有個效能問題.
    關於"synchronized"的構造有相當多的文獻([1],[3]).對內建鎖的優化主要集中在最小化空間開銷
(因為每個Java物件都有一個監視器)和單處理器中上下文的時間開銷.
    這兩個工作對同步器都不是特別重要:1.程式設計師只在需要的時候構造同步器,建立監視器。2.越來越多的程式
執行在多核多執行緒的環境下,資源競爭是不可避免的。
    通常的JVM策略只對  "zero-contention(零爭用)" 情況進行優化鎖,將其他情況留給不那麼可預測的慢路徑[12].
複製程式碼

synchronized: jdk1.6以後對synchronized鎖進行了優化,包含偏向鎖、輕量級鎖、自旋鎖、重量級鎖

    JUC的主要效能目標是可伸縮性: 在爭用同步器時,可以預測並保持效率。理想狀態下無論多少線
程通過同步點,通過同步點所需的開銷都應該是常量. 其中主要目標是縮小"允許通過同步點但是還未通
過的總時間"(減少"臨界區"的空閒時間).
實現目標的同時需要考慮資源上的平衡: 總的CPU時間,記憶體流量,執行緒排程開銷. 例如:自旋鎖通常
比阻塞鎖耗時更短,但通常會CPU空轉併產生記憶體爭用,短時自旋長時阻塞.
    兩種風格: 大多數應用程式應該最大限度地提高總吞吐量,容忍一定概率的飢餓度。 在資源控制等
應用程式中,最重要的是保證執行緒間訪問的公平性,容忍較差的聚合吞吐量。沒有一個框架能夠代表使用者
在這些衝突的目標之間做出決定;公平政策需要交由使用者選擇。
    不管同步器在內部設計得有多好,它都會遇到效能瓶頸。因此,框架提供監視和檢查基本操作,方便
允許使用者發現和緩解瓶頸,提供一種方法來確定有多少執行緒被阻塞是最有效的。
複製程式碼

允許通過但還沒通過: 同步狀態已經可以被執行緒獲取,但是沒有執行緒來獲取
key word: 資源平衡 記憶體爭用 公平/非公平 監視和檢查

3.設計和實現

同步器背後的基本思想非常簡單。
acquire 操作流程:
    while (同步狀態不允許acquire) {
        當前執行緒入隊,若它還不在佇列中
        當前執行緒可能阻塞
    }
    當前執行緒出隊如果在佇列中

release 操作流程:
    更新同步狀態
    if (狀態允許阻塞執行緒獲取)
        解鎖一個或多個佇列中的的執行緒
複製程式碼
流程需要協調三個基本元件間協調:
    原子管理同步狀態
    阻塞/喚醒執行緒
    維護佇列
    
    也許可以建立一個框架允許三個元件獨立變化,但效果不太好.例如: 節點中儲存的資訊必須與喚醒需要的資訊相匹配,對外開放的方法需要依賴同步狀態.
    同步器框架設計的核心是如何選擇這三個元件的具體實現。
複製程式碼

3.1 同步狀態 Synchronization State

Class AbstractQueuedSynchronizer 使用(32bit)int state來維護同步狀態,對外開放getState、setState
和 compareAndSetState來訪問和更新state。這些方法依賴於JSR-133(jmm)支援提供的 volatile 讀寫,以及通過訪問本地指令
compare-and-swap or loadlinked/store-conditional 實現 compareAndSetState,只有當狀態等於給定的期望值時,才原子地
將狀態設定為給定的新值。
複製程式碼
同步狀態為32位int型。雖然JSR-166提供了long原子操作,但是必須在足夠多的平臺上使用內部鎖來模擬原子操作,導致同步不
能很好地執行。在將來可能出現基於64bit的基類,目前32bit已經足夠。只有CyclicBarrier需要64bit,所以CyclicBarrier內部使
用了Lock(就像JUC中大多數高階工具)
複製程式碼
基於AbstractQueuedSynchronizer的具體類必須根據這些對外開放的狀態方法,重寫tryAcquire和tryRelease方法,以便控制acquire and release操作。如果,tryAcquire方法返回true,則獲得同步;如果tryRelease 方法返回 true,則同步狀態允許其他執行緒獲取同步。這些方法接受一個int引數,該引數可用於通訊所需的狀態;例如,在重入鎖中,重新獲取鎖時會重新建立遞迴計數,許多同步器不需要這樣的引數,所以忽略它。
複製程式碼

3.2 阻塞/喚醒

JSR-166(jdk 1.5) 之前只有基於監視器阻塞/喚醒的 java api 來建立同步器. 唯一的方案Thread.suspend and 
Thread.resume,有兩個問題: 1. 不會釋放鎖,2. 呼叫順序顛倒,比如先resume再suspend,執行緒會永遠掛起
複製程式碼

對於第一個方案不會釋放鎖,java在1.5中增加了obj.wait/obj.notify 方法,效能上park,unpark更高一些,場景上aqs不基於監視器使用鎖

JUC.locks LockSupport 針對第二個問題提供了 LockSupport.park() 和 LockSupport.unpark().方法park阻塞當前執行緒,
除非或直到發出unpark(允許虛假覺醒). unpark不"計數",在park前無論呼叫多少次"unpark"只能喚醒一次"park"複製程式碼
這個簡單的機制在某種程度上類似於solars -9執行緒庫[11]、WIN32“消耗性事件”和LinuxNPTL執行緒庫中使用的機制,因此可以
有效地對映到Java執行的最常見平臺上的每一個執行緒庫,提供良好的執行效能。(Solaris和Linux上當前的 "Sun Hotspot JVM"
實際上使用了 "pthread condvar" 來適應當前的執行時設計)。park 支援相對超時和絕對超時,並與 JVM thread.interrupt
整合在一起————'中斷一個執行緒並喚醒他'(執行緒呼叫阻塞方法時丟擲異常)
複製程式碼

park: hg.openjdk.java.net/aarch32-por… park/unpark在linux環境下使用了POSIX多程式

3.3 佇列

框架的核心是維護阻塞執行緒的佇列,這裡將其限制為FIFO佇列。因此,aqs不支援基於優先順序的同步。毫無爭議,非阻塞資料結構
是實現同步佇列最好的選擇,這些資料結構不需要使用"lower-level"鎖來構造。
非阻塞資料結構有兩個選擇: MCS [9] 和CLH [5][8][10]。歷史上,CLH只被用於過自旋鎖。對於同步器框架CLH更容易使用,因
為CLH更易於處理取消和超時,所以選了CLH作為同步佇列的基礎. 最終實現和CLH相差甚遠。
CLH不是很像佇列,因為CLH出隊入隊的操作跟它作為鎖的特性密切相關。它是一個連結串列佇列,通過原子操作更新head和tail,兩個
節點指向虛擬節點。
複製程式碼

新節點,"node",使用原子性操作入隊:
    do { 
        pred = tail;
    } while(!tail.compareAndSet(pred,node));
複製程式碼
每個節點能否拿到鎖,依賴於前一個節點的狀態:
    while (pred.status != RELEASED) ; // spin
複製程式碼
通過自旋之後的出隊操作,將當前節點設定為 "head":
    head = node;
複製程式碼
CLH鎖的優點之一是,入隊和出隊速度快,無鎖,無阻塞(即使在爭用狀態下,一個執行緒也總是會在插入競爭中獲勝,從而取得進展);
檢查有沒有執行緒在等待也很簡單(判斷head和tail是否是一樣),而且RELEASED狀態是分散開的,減少競爭。
CLH的最初版本,節點之間甚至沒有連線。在自旋鎖中,pred被作為一個區域性變數。Scott and Scherer[10] 發現顯式的維護前置
節點,CLH能處理超時和節點取消(如果上一個節點取消,跳過這個節點)。
複製程式碼
對CLH的主要修改是為一個節點提供方法定位後續節點。在自旋鎖中,節點只需要修改自身狀態,後繼節點就能注意到。所以不需要
連線後繼節點。但是在阻塞同步器中,節點需要顯式地park(unpark)它的後繼者。
AQS的節點有一個next屬性關聯到它的後繼節點。但是沒有什麼技術可以"無鎖地""原子地"向雙向佇列中插入一個節點,所以這個屬
性沒有作為原子性插入節點的一部分(pred與next只需要保證一個原子更新成功即可,另一個直接賦值):
    pred.next = node;
next連線通常只被當做一種優化的訪問路徑。如果一個節點的後繼節點不存在了(可能被取消了),可以通過從tail遍歷每個節點的
pred屬性找到一個實際存在的節點。
複製程式碼
第二個改動,每個節點中使用一個status屬性來控制執行緒的阻塞和喚醒。同步器框架中,排隊的執行緒只有通過 tryAcquire 方法才能
在 acquire 操作中返回; 只有 RELEASED 狀態已經不夠用了。
1.確保活動的執行緒在head才允許呼叫 tryAcquire; 這種情況下可能獲取失敗,重新阻塞。
2.這種情況可以通過檢查前一個節點是否是head判斷是否呼叫tryAcquire。
與自旋鎖相比,這樣減少了讀取head的記憶體爭用(只讀取一次,而自旋鎖在一直迴圈讀取)。但是,取消的狀態還是需要在節點中維護。
複製程式碼
佇列節點狀態列位還用於避免不必要的對park和unpark的呼叫。雖然這些方法與阻塞原語(blocking primitives 作業系統級別)效能
相當,但呼叫park時在 JAVA|JVM|OS 之間會有一些無法避免的開銷。執行緒先設定一個"signal me"節點,呼叫park之前檢查同步和節
點狀態,若realse執行緒呼叫unpark且重置同步狀態,避免了不必要的阻塞(阻塞喚醒 比較耗時)。
aqs實現CLH,依賴垃圾收集來管理節點的儲存回收,避免了複雜性和開銷。退出佇列時,將確定不再用到的屬性置為null. help GC

還有一些更進一步的小優化,包括將head和tail的初始化延遲到第一次有執行緒等待時。
複製程式碼
忽略這些細節,對於基本的acquire(排他,不響應中斷,不超時)的大致實現:
if (!tryAcquire(arg)) {
    node = create and enqueue new node;  // 建立 入隊 新節點
    pred = node effective predecessor; // 有效前節點
    while (pred is not head node || !tryAcquire(arg)) {
        if (pred signal bit is set)   // pred 狀態為 signal
            park();                     // 阻塞
        else
            compareAndSet pred signal bit to true;    // 將前節點狀態改為signal
            pred = node effective predecessor;        // 找到有效前節點
    }
    head = node;                        // pred is head node && tryAcquire(arg)
}

release :
if (tryRelease(arg) && head node signal bit is set) {
    compareAndSet head signal bit to false;   // cas head signal 
    unpark head successor,if one exists
}
複製程式碼
雖然acquire中tryAcquire可能會迴圈多次,但是,如果不考慮被取消的節點和每個平臺對於park
的不同實現的話,acquire和release中的單個操作分攤到每個執行緒中都是常量時間O(1)。
喚醒後需要檢查超時和中斷時支援取消功能,因為超時或者中止而取消的執行緒設定節點狀態並且喚醒
下一個節點。因為有取消節點存在,找到有效的前節點,後節點以及重置狀態可能會花費O(n)次的遍歷
(n是佇列長度). 因為執行緒不會再次阻塞以取消的操作,所以連結和狀態很快的重建
複製程式碼

3.4 Condition Queues

同步框架提供 "ConditionObject"類 供同步器實現獨享鎖,並且遵循"Lock"介面的API。一個鎖可以
繫結多個條件物件。提供經典的 monitor-style await,signal,and signalAll,以及這些操作的
超時版本,檢查和監視的方法
"ConditionObject" 類可以用來與其他同步器配合,修復一些設計缺陷。擁有"condition"的鎖被當前
執行緒持有時(see[4]替代方案),提供"Java-style monitor"訪問規則,不同之處只在於方法名稱、額
外功能以及使用者可以為每個鎖宣告多個條件。
"ConditionObject" 使用了和同步器相同的內部佇列節點,但是在一個單獨的條件佇列中維護它們。
signal 操作將節點從條件佇列轉移到鎖佇列,而不是喚醒等待執行緒.
基本 "await" 操作: 
    在條件佇列中建立新增鎖;
    釋放鎖;
    阻塞直到節點移動到鎖佇列;
    re-acquire鎖;
"signal" 操作: 
    從等待佇列轉移到鎖佇列
複製程式碼
因為這些操作只在持有鎖時執行,他們可以使用順序連結串列操作(nextWaiter)維持等待佇列,傳輸操作斷
開等待佇列的第一個節點,然後使用CLH插入將其附加到鎖佇列。
實現這些操作的主要複雜性在於處理因為超時和Thread.interrupt引起的 condition waits取消.取消
操作如果與signal操作發生的時間非常接近的話,可能會產生競爭,它們的競爭結果符合Java內建monitor
(built-in monitors)的規範。 JSR-133修訂版中,如果中斷髮生在signal之前,那麼await必須在被
喚醒時丟擲InterruptedException;如果中斷髮生在signal之後,那麼await必須無異常地返回,但是
需要設定它的執行緒中斷狀態。
複製程式碼
為了保持順序,佇列節點狀態記錄是否正在(已經)傳輸.signal和cancel都會去CAS這個狀態.如果signal
失敗,signal下一個節點(如果有的話)。如果取消操作失敗,則必須中止傳輸,然後重新獲取await lock.
後一種情況引入了潛在的無界自旋。等待取消不能獲取鎖直到節點成功的插入lock 佇列,因此必須自旋
等待 signalling thread 執行cas 插入CLH佇列成功. 這裡很少需要自旋,佔用一個執行緒. 這裡使用了
Thread.yield讓出取消節點的執行緒的CPU時間片。理想情況下正好被signal執行緒輪轉到,那麼顯然自旋很
快就會結束.雖然可以實現一些策略來幫助解決取消節點的自旋,但是這種情形太少見了,沒有多大意義,反而會增加些額外的消耗. 在其他任何情形下,都不會有自旋或yield,所以在單核的情況下能夠保證
不錯的效能。
複製程式碼

4. 使用(USAGE)

    Class "AbstractQueuedSynchronizer"使用模板方法,試圖將上述功能綜合在一起作為同步器的基類,子類只需要實現"state"的檢查和更新操作來控制acquire和release即可. 但是這些子類不直接繼承aqs,aqs不能作為同步器ADTs,aqs對外開放了內部控制acquire和release的方法,不應該讓這些類對使用者可見。
所有的JUC同步器宣告瞭private內部aqs子類並將所有同步方法委託給它。
複製程式碼
例如,這裡有一個最小的"Mutex"類,state 0 表示unlocked,1 表示locked。

class Mutex {
  class Sync extends AbstractQueuedSynchronizer {
    public boolean tryAcquire(int ignore) {
      return compareAndSetState(0,1);
    }
    public boolean tryRelease(int ignore) {
      setState(0); return true;
    }
  }
  private final Sync sync = new Sync();
  public void lock() { sync.acquire(0); }
  public void unlock() { sync.release(0); }
}

    此示例的完整版本以及其他使用指南可以在J2SE檔案中找到。當然,有一些變體。例如,tryAcquire可以使用
"test-and-test-and-set". 在cas之前檢查一遍.
複製程式碼

test-and-test-and-set: TTAS自旋鎖演演算法

    令人驚訝的是,互斥鎖這樣對"performance-sensitive"的同步器居然使用"delegation""virtual"的方式
來實現。這些是現代動態編譯器長期關注的"OO"設計結構,他們會優化掉這類開銷,尤其是這些使用頻繁的同步器。
複製程式碼

OO: 面向物件
delegation 委派:一個物件請求另一個物件的功能,捕獲一個操作並將其傳送到另一個物件
virtual 虛擬: 是C語言的虛方法相當於java的抽象方法。例如tryAcquire,子類需要重寫
performance-sensitive: 效能敏感

aqs 提供了各種幫助同步器控制同步的策略。例如: acquire方法包括timeout&interruptible兩種版本。
aqs 提供了 "exclusive-mode""share-mode". share-mode 可以在並且可以通過"cascading signals"
喚醒多個等待的執行緒
複製程式碼

cascading signals 級聯訊號:級聯(cascade)在電腦科學裡指多個物件之間的對映關係,建立資料之間的級聯關係提高管理效率。aqs的級聯訊號是,節點狀態 static final int PROPAGATE = -3; 當前節點狀態為PROPAGATE,下一個節點無條件喚醒。

    雖然序列化(永久儲存 或 傳輸)同步器是不明智的,但這些類常被用來構造其他類。例如執行緒安全的集合,他們
通常需要序列化。
    aqs ConditionObject classes 提供序列化"同步狀態"的方法,但是沒有序列化阻塞佇列和瞬時狀態.大部分
"同步器"反序列化時都重置"同步狀態"到初始值,這與"build-in monitor"的反序列化策略一致:總是反序列化到
未鎖狀態。這相當於"no-op",但必須顯示支援,為了final field的反序列化.
複製程式碼

no-op:代表沒有操作. 是一個彙編指令,佔據很少的空間但是不做任何操作。

4.1 公平性 Controlling Fairness

參考資料:m.aliyun.com/yunqi/artic…

    即使同步器是先進先出佇列,但不一定公平。注意基本的acquire演演算法(章節3.3),tryAcquire
在入隊前檢查&執行.如果恰好在解鎖時有一個執行緒tryAcquire,會"steal(竊取)"隊首的訪問許可權
複製程式碼

steal:竊取. 竊取訪問,更充分的使用臨界區,但會造成飢餓

這種"barging FIFO"策略通常比其他技術提供更高的總吞吐量.在release期間允許Acquire獲取鎖,減少整體時間.同時,release時它只喚醒隊首的執行緒避免過多的、無益的爭用。
複製程式碼

爭用: 多個執行緒同時訪問同一資料(記憶體區域),其中一次是修改操作

如果傳入的執行緒到達的速度快於unpark喚醒執行緒的速度,隊首贏得競爭的機會很小,會導致幾乎總是
再次阻塞. 對於"briefly-held"同步器,在第一個執行緒喚醒期間,多處理器上通常會發生多個bargings
和release. 淨效應來看,barging策略維持了一個或多條執行緒的高速率,一定概率上避免了飢餓.
複製程式碼

淨效應: 一個經濟行為可能會產生正的效應和負的效應,淨效應是兩者相抵以後的效應. 在文字中可以理解為綜上所述 briefly-held:瞬間持有,指持有鎖的時間非常短遠小於從阻塞狀態到被喚醒所消耗的時間.

當有更高的公平性需求時,實現起來也很簡單。如果需要嚴格的公平性,程式設計師可以把tryAcquire方法定義為,若當
前執行緒不是佇列的頭節點(可通過getFirstQueuedThread方法檢查,這是框架提供的為數不多的幾個檢測方法之一),則立即失敗(返回false)。
一個快一些但不太嚴格的變體若佇列為空,允許tryAcquire執行成功。這種情況下多個執行緒同時遇到會去競爭以便使
自己第一個獲得鎖,此時,通常至少有一個執行緒無需入隊,所有JUC同步器都採用這種公平策略.
複製程式碼
'JLS' 不提供這樣對公平性的排程保證,只能使用者自己實現。即使是嚴格公平的同步器,若一組執行緒不使用阻塞,JVM會
按照自己的排程規則執行這些執行緒("JVM執行緒排程").在單核處理器中執行緒執行完一個時間片後會切換上下文。如果有一
條執行緒持有鎖,只有等它釋放鎖並喚醒下一個等待節點.在這條執行緒在執行完任務,不再需要鎖但是還沒釋放的時候有可能
被切換,因為是公平策略,只能等這條執行緒再次執行釋放鎖然後喚醒等待佇列中的下一個節點。因此JVM執行緒排程無形之中
增加了鎖的空閒時間。
同步器公平模式在多核處理器系統架構往往產生更大影響. 如果是8核,8等1,複製程式碼

JLS:Java Language Specification,java語言規範 docs.oracle.com/javase/spec…
uniprocessor: 單核處理器
JVM執行緒排程: 搶佔式排程,每個執行緒將由系統來分配執行時間,執行緒的切換不由執行緒本身來決定(Java中,可以通過Thread.yield()讓出執行時間,但無法獲取執行時間)。執行緒執行時間系統可控,也不會有一個執行緒導致整個程式阻塞。

儘管公平鎖在'briefly-held''高爭用'的場景下,表現糟糕。也有公平鎖表現良好的時候,例如當它們保護相對較長的
程式碼體和/或互鎖間隔長時,這種情況下barging帶來的提神很少,反而會增加"飢餓"的風險。使用者可以根據具體場景選擇
適合的"同步器"型別。
複製程式碼

briefly-held: 鎖都在極短的時間間隔下被持有(高頻率的使用鎖)
高爭用: 相關的深度好文 www.sohu.com/a/232590588… 鎖不慢;鎖競爭慢

4.2 同步器 Synchronizers

這一章節是簡介了 J.U.C中哪些同步器使用了aqs框架:

ReentrantLock 可重入鎖 : 使用同步狀態(state) 記錄執行緒遞迴呼叫鎖的次數。獲得鎖時記錄當前執行緒id,以便遞迴計數和
試圖解鎖時檢查執行緒為當前執行緒。這個類也使用了ConditionObject,和其他監測和檢驗方法。提供公平和非公平(預設)模式,可用於替代同步關鍵字(synchronizer)

ReentrantReadWriteLock 可衝入讀寫鎖: 使用同步狀態其中16bits維護寫鎖計數,另外16bits維護讀鎖。WriteLock類似於
ReentrantLock,ReadLock 使用acquireShared 方法支援多讀

Semaphore: 使用同步狀態計數,使用acquireShared 減少計數,若state <= 0則執行緒阻塞,使用 tryRelease
增加計數(如果釋放後計數為整數)喚醒等待節點

CountDownLatch: 使用同步狀態表示計數。當計數到達零,所有執行緒同時釋放鎖執行之後的程式碼

FutureTask(jdk 1.8中沒使用aqs):  使用同步變量表示Future的執行狀態(初始、執行、取消、完成)。呼叫get()使用park
阻塞呼叫執行緒. 當執行狀態轉變為 取消|完成,任務完成狀態喚醒阻塞執行緒。

SynchronousQueue: "CSP-style handoff",執行緒間同步物件。使用內部等待節點匹配生產者消費者,它使用同步狀態
允許一個生產者在一個消費者消費了物件之後繼續生產下一個物件,反之亦然。

使用者可以使用J.U.C自定義同步器.例如:很多曾經被考慮過但是沒有被採納進J.U.C的類,它們提供了與 
WIN32 events,binary latches,centrally managed locks,and tree-based barriers 類似的語義和功能。
複製程式碼