本文首發於一世流雲的專欄: https://segmentfault.com/blog...
AbstractQueuedSynchronizer抽象類(如下簡稱AQS)是整個java.util.concurrent
包的核心。在JDK1.5時,Doug Lea引入了J.U.C包,該包中的大多數同步器都是基於AQS來構建的。AQS框架提供了一套通用的機制來管理同步狀態(synchronization state)、阻塞/喚醒線程、管理等待隊列。java
咱們所熟知的ReentrantLock、CountDownLatch、CyclicBarrier等同步器,其實都是經過內部類實現了AQS框架暴露的API,以此實現各種同步器功能。這些同步器的主要區別其實就是對同步狀態(synchronization state)的定義不一樣。node
AQS框架,分離了構建同步器時的一系列關注點,它的全部操做都圍繞着資源——同步狀態(synchronization state)來展開,並替用戶解決了以下問題:segmentfault
這實際上是一種典型的模板方法設計模式:父類(AQS框架)定義好骨架和內部操做細節,具體規則由子類去實現。
AQS框架將剩下的一個問題留給用戶:
什麼是資源?如何定義資源是否能夠被訪問?設計模式
咱們來看下幾個常見的同步器對這一問題的定義:安全
同步器 | 資源的定義 |
---|---|
ReentrantLock | 資源表示獨佔鎖。State爲0表示鎖可用;爲1表示被佔用;爲N表示重入的次數 |
CountDownLatch | 資源表示倒數計數器。State爲0表示計數器歸零,全部線程均可以訪問資源;爲N表示計數器未歸零,全部線程都須要阻塞。 |
Semaphore | 資源表示信號量或者令牌。State≤0表示沒有令牌可用,全部線程都須要阻塞;大於0表示由令牌可用,線程每獲取一個令牌,State減1,線程沒釋放一個令牌,State加1。 |
ReentrantReadWriteLock | 資源表示共享的讀鎖和獨佔的寫鎖。state邏輯上被分紅兩個16位的unsigned short,分別記錄讀鎖被多少線程使用和寫鎖被重入的次數。 |
綜上所述,AQS框架提供瞭如下功能:併發
因爲併發的存在,須要考慮的狀況很是多,所以可否以一種相對簡單的方法來完成這兩個目標就很是重要,由於對於用戶(AQS框架的使用者來講),不少時候並不關心內部複雜的細節。而AQS其實就是利用模板方法模式來實現這一點,AQS中大多數方法都是final或是private的,也就是說Doug Lea並不但願用戶直接使用這些方法,而是隻覆寫部分模板規定的方法。
AQS經過暴露如下API來讓讓用戶本身解決上面提到的「如何定義資源是否能夠被訪問」的問題:框架
鉤子方法 | 描述 |
---|---|
tryAcquire | 排它獲取(資源數) |
tryRelease | 排它釋放(資源數) |
tryAcquireShared | 共享獲取(資源數) |
tryReleaseShared | 共享獲取(資源數) |
isHeldExclusively | 是否排它狀態 |
還記得Lock接口中的那些鎖中斷、限時等待、鎖嘗試的方法嗎?這些方法的實現其實AQS都內置提供了。
使用了AQS框架的同步器,都支持下面的操做:工具
Condition接口,能夠看作是Obejct類的wait()、notify()、notifyAll()方法的替代品,與Lock配合使用。
AQS框架內部經過一個內部類ConditionObject
,實現了Condition接口,以此來爲子類提供條件等待的功能。ui
在本章第一部分講到,AQS利用了模板方法模式,其中大多數方法都是final或是private的,咱們把這類方法稱爲Skeleton Method,也就是說這些方法是AQS框架自身定義好的骨架,子類是不能覆寫的。
下面會按類別簡述一些比較重要的方法,具體實現細節及原理會在本系列後續部分詳細闡述。this
CAS,即CompareAndSet,在Java中CAS操做的實現都委託給一個名爲UnSafe類,關於Unsafe
類,之後會專門詳細介紹該類,目前只要知道,經過該類能夠實現對字段的原子操做。
方法名 | 修飾符 | 描述 |
---|---|---|
compareAndSetState | protected final | CAS修改同步狀態值 |
compareAndSetHead | private final | CAS修改等待隊列的頭指針 |
compareAndSetTail | private final | CAS修改等待隊列的尾指針 |
compareAndSetWaitStatus | private static final | CAS修改結點的等待狀態 |
compareAndSetNext | private static final | CAS修改結點的next指針 |
方法名 | 修飾符 | 描述 |
---|---|---|
enq | private | 入隊操做 |
addWaiter | private | 入隊操做 |
setHead | private | 設置頭結點 |
unparkSuccessor | private | 喚醒後繼結點 |
doReleaseShared | private | 釋放共享結點 |
setHeadAndPropagate | private | 設置頭結點並傳播喚醒 |
方法名 | 修飾符 | 描述 |
---|---|---|
cancelAcquire | private | 取消獲取資源 |
shouldParkAfterFailedAcquire | private static | 判斷是否阻塞當前調用線程 |
acquireQueued | final | 嘗試獲取資源,獲取失敗嘗試阻塞線程 |
doAcquireInterruptibly | private | 獨佔地獲取資源(響應中斷) |
doAcquireNanos | private | 獨佔地獲取資源(限時等待) |
doAcquireShared | private | 共享地獲取資源 |
doAcquireSharedInterruptibly | private | 共享地獲取資源(響應中斷) |
doAcquireSharedNanos | private | 共享地獲取資源(限時等待) |
方法名 | 修飾符 | 描述 |
---|---|---|
acquire | public final | 獨佔地獲取資源 |
acquireInterruptibly | public final | 獨佔地獲取資源(響應中斷) |
acquireInterruptibly | public final | 獨佔地獲取資源(限時等待) |
acquireShared | public final | 共享地獲取資源 |
acquireSharedInterruptibly | public final | 共享地獲取資源(響應中斷) |
tryAcquireSharedNanos | public final | 共享地獲取資源(限時等待) |
方法名 | 修飾符 | 描述 |
---|---|---|
release | public final | 釋放獨佔資源 |
releaseShared | public final | 釋放共享資源 |
咱們在第一節中講到,AQS框架分離了構建同步器時的一系列關注點,它的全部操做都圍繞着資源——同步狀態(synchronization state)來展開所以,圍繞着資源,衍生出三個基本問題:
同步狀態的定義
同步狀態,其實就是資源。AQS使用單個int(32位)來保存同步狀態,並暴露出getState、setState以及compareAndSetState操做來讀取和更新這個狀態。
/** * 同步狀態. */ private volatile int state; protected final int getState() { return state; } protected final void setState(int newState) { state = newState; } /** * 以原子的方式更新同步狀態. * 利用Unsafe類實現 */ protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
在JDK1.5以前,除了內置的監視器機制外,沒有其它方法能夠安全且便捷得阻塞和喚醒當前線程。
JDK1.5之後,java.util.concurrent.locks包提供了LockSupport類來做爲線程阻塞和喚醒的工具。
等待隊列,是AQS框架的核心,整個框架的關鍵其實就是如何在併發狀態下管理被阻塞的線程。
等待隊列是嚴格的FIFO隊列,是Craig,Landin和Hagersten鎖(CLH鎖)的一種變種,採用雙向鏈表實現,所以也叫CLH隊列。
1. 結點定義
CLH隊列中的結點是對線程的包裝,結點一共有兩種類型:獨佔(EXCLUSIVE)和共享(SHARED)。
每種類型的結點都有一些狀態,其中獨佔結點使用其中的CANCELLED(1)、SIGNAL(-1)、CONDITION(-2),共享結點使用其中的CANCELLED(1)、SIGNAL(-1)、PROPAGATE(-3)。
結點狀態 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 取消。表示後驅結點被中斷或超時,須要移出隊列 |
SIGNAL | -1 | 發信號。表示後驅結點被阻塞了(當前結點在入隊後、阻塞前,應確保將其prev結點類型改成SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。) |
CONDITION | -2 | Condition專用。表示當前結點在Condition隊列中,由於等待某個條件而被阻塞了 |
PROPAGATE | -3 | 傳播。適用於共享模式(好比連續的讀操做結點能夠依次進入臨界區,設爲PROPAGATE有助於實現這種迭代操做。) |
INITIAL | 0 | 默認。新結點會處於這種狀態 |
AQS使用CLH隊列實現線程的結構管理,而CLH結構正是用前一結點某一屬性表示當前結點的狀態,之因此這種作是由於在雙向鏈表的結構下,這樣更容易實現取消和超時功能。
next指針:用於維護隊列順序,當臨界區的資源被釋放時,頭結點經過next指針找到隊首結點。
prev指針:用於在結點(線程)被取消時,讓當前結點的前驅直接指向當前結點的後驅完成出隊動做。
static final class Node { // 共享模式結點 static final Node SHARED = new Node(); // 獨佔模式結點 static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; /** * INITAL: 0 - 默認,新結點會處於這種狀態。 * CANCELLED: 1 - 取消,表示後續結點被中斷或超時,須要移出隊列; * SIGNAL: -1- 發信號,表示後續結點被阻塞了;(當前結點在入隊後、阻塞前,應確保將其prev結點類型改成SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。) * CONDITION: -2- Condition專用,表示當前結點在Condition隊列中,由於等待某個條件而被阻塞了; * PROPAGATE: -3- 傳播,適用於共享模式。(好比連續的讀操做結點能夠依次進入臨界區,設爲PROPAGATE有助於實現這種迭代操做。) * * waitStatus表示的是後續結點狀態,這是由於AQS中使用CLH隊列實現線程的結構管理,而CLH結構正是用前一結點某一屬性表示當前結點的狀態,這樣更容易實現取消和超時功能。 */ volatile int waitStatus; // 前驅指針 volatile Node prev; // 後驅指針 volatile Node next; // 結點所包裝的線程 volatile Thread thread; // Condition隊列使用,存儲condition隊列中的後繼節點 Node nextWaiter; Node() { } Node(Thread thread, Node mode) { this.nextWaiter = mode; this.thread = thread; } }
2. 隊列定義
對於CLH隊列,當線程請求資源時,若是請求不到,會將線程包裝成結點,將其掛載在隊列尾部。
CLH隊列的示意圖以下:
①初始狀態,隊列head和tail都指向空
②首個線程入隊,先建立一個空的頭結點,而後以自旋的方式不斷嘗試插入一個包含當前線程的新結點
/** * 以自旋的方式不斷嘗試插入結點至隊列尾部 * * @return 當前結點的前驅結點 */ private Node enq(final Node node) { for (; ; ) { Node t = tail; if (t == null) { // 若是隊列爲空,則建立一個空的head結點 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
本章簡要介紹了AQS的思想和原理,讀者能夠參考Doug Lea的論文,進一步瞭解AQS。直接閱讀AQS的源碼比較漫無目的,後續章節,將從ReentrantLock、CountDownLatch的使用入手,講解AQS的獨佔功能和共享功能。