AQS 全稱是 AbstractQueuedSynchronizer,顧名思義,是一個用來構建鎖和同步器的框架,它底層用了 CAS 技術來保證操作的原子性,同時利用 FIFO 隊列實現線程間的鎖競爭,將基礎的同步相關抽象細節放在 AQS,這也是 ReentrantLock、CountDownLatch 等同步工具實現同步的底層實現機制。它能夠成爲實現大部分同步需求的基礎,也是 J.U.C 併發包同步的核心基礎組件。
AQS 結構剖析
AQS 就是建立在 CAS 的基礎之上,增加了大量的實現細節,例如獲取同步狀態、FIFO 同步隊列,獨佔式鎖和共享式鎖的獲取和釋放等等,這些都是 AQS 類對於同步操作抽離出來的一些通用方法,這麼做也是爲了對實現的一個同步類屏蔽了大量的細節,大大降低了實現同步工具的工作量,這也是爲什麼 AQS 是其它許多同步類的基類的原因。
現在我們來直接定位到類 java.util.concurrent.locks.AbstractQueuedSynchronizer,下面是 AQS 類的幾個重要字段與方法列出來:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
// ...
}
1.head 字段爲等待隊列的頭節點,表示當前正在執行的節點;2.tail 字段爲等待隊列的尾節點;3.state 字段爲同步狀態,其中 state > 0 爲有鎖狀態,每次加鎖就在原有 state 基礎上加 1,即代表當前持有鎖的線程加了 state 次鎖,反之解鎖時每次減一,當 statte = 0 爲無鎖狀態;4.通過 compareAndSetState 方法操作 CAS 更改 state 狀態,保證 state 的原子性。
有沒有發現,這幾個字段都用 volatile 關鍵字進行修飾,以確保多線程間保證字段的可見性。
AQS 提供了兩種鎖,分別是獨佔鎖和共享鎖,獨佔鎖指的是操作被認作一種獨佔操作,比如 ReentrantLock,它實現了獨佔鎖的方法,而共享鎖則指的是一個非獨佔操作,比如一些同步工具 CountDownLatch 和 Semaphore 等同步工具,下面是 AQS 對這兩種鎖提供的抽象方法。
獨佔鎖:
// 獲取鎖方法
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 釋放鎖方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
共享鎖:
// 獲取鎖方法
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 釋放鎖方法
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
在我們平時開發中,基本不用直接使用 AQS,我們平時都是直接使用 JDK 自帶的同步類工具,如 ReentrantLock、CountDownLatch 和 Semaphore 等,它們已經可以滿足絕大部分的需求了,後面會抽幾篇文章單獨講一下這些同步類工具是如何使用 AQS 的,這對於我們如何構建自定義的同步工具,有很大的幫助。
下面是同步隊列節點的結構:
用大神的註釋來形象地描述一下隊列的模型:
/**
* <pre>
* +------+ prev +-----+ +-----+
* head | | <---- | | <---- | | tail
* +------+ +-----+ +-----+
* </pre>
*/
這是一個普通雙向鏈表的節點結構,多了 thread 字段用於存儲當前線程對象,同時每個節點都有一個 waitStatus 等待狀態,一共有四種狀態:
1.CANCELLED(1):取消狀態,如果當前線程的前置節點狀態爲 CANCELLED,則表明前置節點已經等待超時或者已經被中斷了,這時需要將其從等待隊列中刪除。2.SIGNAL(-1):等待觸發狀態,如果當前線程的前置節點狀態爲 SIGNAL,則表明當前線程需要阻塞。3.CONDITION(-2):等待條件狀態,表示當前節點在等待 condition,即在 condition 隊列中。4.PROPAGATE(-3):狀態需要向後傳播,表示 releaseShared 需要被傳播給後續節點,僅在共享鎖模式下使用。
可以這麼理解:head 節點可以表示成當前持有鎖的線程的節點,其餘線程競爭鎖失敗後,會加入到隊尾,tail 始終指向隊列的最後一個節點。
AQS 的結構大概可總結爲以下 3 部分:
1.用 volatile 修飾的整數類型的 state 狀態,用於表示同步狀態,提供 getState 和 setState 來操作同步狀態;2.提供了一個 FIFO 等待隊列,實現線程間的競爭和等待,這是 AQS 的核心;3.AQS 內部提供了各種基於 CAS 原子操作方法,如 compareAndSetState 方法,並且提供了鎖操作的acquire和release方法。
獨佔鎖
獨佔鎖的原理是如果有線程獲取到鎖,那麼其它線程只能是獲取鎖失敗,然後進入等待隊列中等待被喚醒。
獲取鎖
獲取獨佔鎖方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
源碼解讀:
1.通過 tryAcquire(arg) 方法嘗試獲取鎖,這個方法需要實現類自己實現獲取鎖的邏輯,獲取鎖成功後則不執行後面加入等待隊列的邏輯了;2.如果嘗試獲取鎖失敗後,則執行 addWaiter(Node.EXCLUSIVE) 方法將當前線程封裝成一個 Node 節點對象,並加入隊列尾部;3.把當前線程執行封裝成 Node 節點後,繼續執行 acquireQueued 的邏輯,該邏輯主要是判斷當前節點的前置節點是否是頭節點,來嘗試獲取鎖,如果獲取鎖成功,則當前節點就會成爲新的頭節點,這也是獲取鎖的核心邏輯。
基於上面源碼的步驟分析後,我們一步步往下看源碼具體實現:
private Node addWaiter(Node mode) {
// 創建一個基於當前線程的節點,該節點是 Node.EXCLUSIVE 獨佔式類型
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
// 這裏先判斷隊尾是否爲空,如果不爲空則直接將節點加入隊尾
if (pred != null) {
node.prev = pred;
// 採取 CAS 操作,將當前節點設置爲隊尾節點,由於採用了 CAS 原子操作,無論併發怎麼修改,都有且只有一條線程可以修改成功,其餘都將執行後面的enq方法
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
簡單來說 addWaiter(Node mode) 方法做了以下事情:
1.創建基於當前線程的獨佔式類型的節點;2.利用 CAS 原子操作,將節點加入隊尾。
我們繼續看 enq(Node node) 方法:
private Node enq(final Node node) {
// 自旋操作
for (;;) {
Node t = tail;
// 如果隊尾節點爲空,那麼進行CAS操作初始化隊列
if (t == null) {
// 這裏很關鍵,即如果隊列爲空,那麼此時必須初始化隊列,初始化一個空的節點表示隊列頭,用於表示當前正在執行的節點,頭節點即表示當前正在運行的節點
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
// 這一步也是採取CAS操作,將當前節點加入隊尾,如果失敗的話,自旋繼續修改直到成功爲止
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq(final Node node) 方法主要做了以下事情:
1.採用自旋機制,這是 aqs 裏面很重要的一個機制;2.如果隊尾節點爲空,則初始化隊列,將頭節點設置爲空節點,頭節點即表示當前正在運行的節點;3.如果隊尾節點不爲空,則繼續採取 CAS 操作,將當前節點加入隊尾,不成功則繼續自旋,直到成功爲止;
對比了上面兩段代碼,不難看出,首先是判斷隊尾是否爲空,先進行一次 CAS 入隊操作,如果失敗則進入 enq(final Node node) 方法執行完整的入隊操作。
完整的入隊操作簡單來說就是:如果隊列爲空,初始化隊列,並將頭節點設爲空節點,表示當前正在運行的節點,然後再將當前線程的節點加入到隊列尾部。
關於隊列的初始化與入隊,務必理解透徹。
經過上面 CAS 不斷嘗試,這時當前節點已經成功加入到隊尾了,接下來就到了acquireQueued 的邏輯,我們繼續往下看源碼:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
// 線程中斷標記字段
boolean interrupted = false;
for (;;) {
// 獲取當前節點的 pred 節點
final Node p = node.predecessor();
// 如果 pred 節點爲 head 節點,那麼再次嘗試獲取鎖
if (p == head && tryAcquire(arg)) {
// 獲取鎖之後,那麼當前節點也就成爲了 head 節點
setHead(node);
p.next = null; // help GC
failed = false;
// 不需要掛起,返回 false
return interrupted;
}
// 獲取鎖失敗,則進入掛起邏輯
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
這一步 acquireQueued(final Node node, int arg) 方法主要做了以下事情:
1.判斷當前節點的 pred 節點是否爲 head 節點,如果是,則嘗試獲取鎖;2.獲取鎖失敗後,進入掛起邏輯。
提醒一點:我們上面也說過,head 節點代表當前持有鎖的線程,那麼如果當前節點的 pred 節點是 head 節點,很可能此時 head 節點已經釋放鎖了,所以此時需要再次嘗試獲取鎖。
接下來繼續看掛起邏輯源碼:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果 pred 節點爲 SIGNAL 狀態,返回true,說明當前節點需要掛起
return true;
// 如果ws > 0,說明節點狀態爲CANCELLED,需要從隊列中刪除
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果是其它狀態,則操作CAS統一改成SIGNAL狀態
// 由於這裏waitStatus的值只能是0或者PROPAGATE,所以我們將節點設置爲SIGNAL,從新循環一次判斷
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
這一步 shouldParkAfterFailedAcquire(Node pred, Node node) 方法主要做了以下事情:
1.判斷 pred 節點狀態,如果爲 SIGNAL 狀態,則直接返回 true 執行掛起;2.刪除狀態爲 CANCELLED 的節點;3.若 pred 節點狀態爲 0 或者 PROPAGATE,則將其設置爲爲 SIGNAL,再從 acquireQueued 方法自旋操作從新循環一次判斷。
通俗來說就是:根據 pred 節點狀態來判斷當前節點是否可以掛起,如果該方法返回 false,那麼掛起條件還沒準備好,就會重新進入 acquireQueued(final Node node, int arg) 的自旋體,重新進行判斷。如果返回 true,那就說明當前線程可以進行掛起操作了,那麼就會繼續執行掛起。
這裏需要注意的時候,節點的初始值爲 0,因此如果獲取鎖失敗,會嘗試將節點設置爲 SIGNAL。
繼續看掛起邏輯:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
LockSupport 是用來創建鎖和其他同步類的基本線程阻塞原語。LockSupport 提供 park() 和 unpark() 方法實現阻塞線程和解除線程阻塞。release 釋放鎖方法邏輯會調用 LockSupport.unPark 方法來喚醒後繼節點。
獲取獨佔鎖流程圖:
釋放鎖
釋放鎖方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
釋放鎖的方法源碼就很好理解,通過 tryRelease(arg) 方法嘗試釋放鎖,這個方法需要實現類自己實現釋放鎖的邏輯,釋放鎖成功後則執行後面的喚醒後續節點的邏輯了,然後判斷 head 節點不爲空並且 head 節點狀態不爲 0,因爲 addWaiter 方法默認的節點狀態爲 0,此時節點還沒有進入就緒狀態。
繼續往下看源碼:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
// 將頭節點的狀態設置爲0
// 這裏會嘗試清除頭節點的狀態,改爲初始狀態
compareAndSetWaitStatus(node, ws, 0);
// 後繼節點
Node s = node.next;
// 如果後繼節點爲null,或者已經被取消了
if (s == null || s.waitStatus > 0) {
s = null;
// for循環從隊列尾部一直往前找可以喚醒的節點
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 喚醒後繼節點
LockSupport.unpark(s.thread);
}
從源碼可看出:釋放鎖主要是將頭節點的後繼節點喚醒,如果後繼節點不符合喚醒條件,則從隊尾一直往前找,直到找到符合條件的節點爲止。
總結
這篇文章主要講述了 AQS 的內部結構和它的同步實現原理,並從源碼的角度深度剖析了AQS 獨佔鎖模式下的獲取鎖與釋放鎖的邏輯,並且從源碼中我們得出:在獨佔鎖模式下,用 state 值表示鎖並且 0 表示無鎖狀態,0 -> 1 表示從無鎖到有鎖,僅允許一條線程持有鎖,其餘的線程會被包裝成一個 Node 節點放到隊列中進行掛起,隊列中的頭節點表示當前正在執行的線程,當頭節點釋放後會喚醒後繼節點,從而印證了 AQS 的隊列是一個 FIFO 同步隊列。
推薦閱讀: