從ReentrantLock看AQS (AbstractQueuedSynchronizer) 執行流程
概述
本文將以ReentrantLock為例來講解AbstractQueuedSynchronizer的執行流程,主要通過原始碼的方式來講解,僅包含大體的執行流程,不會過於深入。
ReentrantLock 介紹
ReentrantLock 是JDK提供的可重入鎖實現類,可用其替換synchronized來實現鎖重入效果;其底層實現主要是依靠AbstractQueuedSynchronizer,本文將通過ReentrantLock來觀察AbstractQueuedSynchronizer的執行流程。
AbstractQueuedSynchronizer
介紹
關於AbstractQueuedSynchronizer(以下簡稱AQS),JDK是這樣子描述的:
Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.
大體意思就是“提供一個框架,用於實現依賴於先進先出(FIFO)等待佇列的阻塞鎖和相關同步器(訊號量、事件等)”,AQS不是一個功能完整的類,而是一個提供了一套依賴於FIFO等待佇列的流程框架,該框架可用於實現鎖等同步器的。AQS中沒有使用任何鎖相關的API,其實現主要依靠CAS (Compare And Swap),是一個優秀的lock-free 程式設計實踐。
AQS資料結構
AQS中主要包含以下三個欄位
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head;
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail;
/**
* The synchronization state.
*/
private volatile int state;
這三個欄位都標記了volatile
,其中head
和 tail
主要用於維護AQS的FIFO雙向佇列,該佇列是AQS的核心,只由AQS該維護該佇列,子類實現不會去維護該佇列;state
用於標記當前同步器的轉態,AQS不會對該欄位做任何操作,該欄位由子類去維護,但AQS提供了修改state
的方法,其中的compareAndSetState
是子類用的最多的,主要用於實現多執行緒對同步器的搶奪。
AQS 主要方法
在開始瞭解AQS的執行流程之前,我們先看一下在使用AQS時需要關注的四個方法:
// 搶奪資源流程的入口,AQS暴露出的API,由自定義同步器來呼叫,ReentrantLock 的lock方法就是去呼叫該方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 由子類實現該方法,搶佔資源邏輯在這個方法實現,該方法由AQS在搶奪資源流程中呼叫。
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 釋放資源流程的入口,AQS暴露出的API,由自定義同步器來呼叫,ReentrantLock 的unlock方法就是去呼叫該方法。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 由子類實現該方法,釋放資源邏輯在這個方法實現,該方法由AQS在釋放資源流程中呼叫。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
其中acquire
和 tryAcquire
跟同步器資源搶奪相關;release
和 tryRelease
和同步器資源釋放相關。acquire
和release
方法是AQS流程的入口,通過這兩個方法來走資源搶奪和資源釋放的流程,該流程中包含了FIFO佇列維護、執行緒狀態管理等操作,是整個AQS的核心,自定義同步器中會去呼叫這兩個方法;而tryAcquire
和tryRelease
方法對應執行緒資源搶佔和釋放操作,這兩個方法中只關心執行緒是否搶佔/釋放資源成功,不會維護FIFO佇列和執行緒狀態,由子類來實現這兩個方法,這個是自定義同步器的核心程式碼,由這兩個方法來實現不同同步器的不同功能。
這四個方法對應的是執行緒獨佔流程,共享流程使用的是
acquireShared
,tryAcquireShared
,releaseShared
,tryReleaseShared
這四個方法,ReentrantLock是執行緒獨佔模式,所以本文主要講解執行緒獨佔流程,但執行緒共享流程和獨佔流程差別不大,感興趣的同學可以自行了解。
下面看一下ReentrantLock中是如何去使用這四個方法的:
// 簡化版程式碼,方便演示
public class ReentrantLock implements Lock, Serializable {
private final Sync sync = new Sync();
public void lock() {
this.sync.acquire(1);
}
public void unlock() {
this.sync.release(1);
}
static final class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 當前鎖沒有被佔用
if (compareAndSetState(0, acquires)) { // 嘗試去拿鎖
setExclusiveOwnerThread(current); // 標記當前執行緒為鎖的持有者
return true; // 返回拿鎖成功
}
}
else if (current == getExclusiveOwnerThread()) { // 當前鎖被佔用,且是被自己佔用,走重入邏輯,對state做累加
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 鎖被佔用或者拿鎖失敗,返回拿鎖失敗
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 鎖被完全釋放
free = true; // 標記鎖已被釋放
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
}
可以看出,ReentrantLock的核心功能是由內部類Sync來完成的,而這個Sync類就是繼承了AQS,並重寫了tryAcquire
和 tryRelease
方法,這兩個方法的實現也很簡單
tryAcquire
中主要是通過state欄位是否等於0來判斷當前鎖是否鎖住了,沒有鎖住,則當前執行緒使用CAS去嘗試將state標記為acquires,標記成功則代表拿鎖成功,返回true
,否則返回false
。tryRelease
中則是通過判斷釋放後state是否為0來判斷當前鎖是否被完全釋放,若完全釋放則返回true
,否則返回false
。
可以看出這個兩個方法的實現只關注鎖的佔用和釋放是否成功,沒有關心FIFO佇列和執行緒狀態。那麼FIFO佇列和執行緒狀態是如何來維護的呢?答案就是在acquire
和release
方法中;從ReentrantLock原始碼也可以看到,lock
和unlock
方法只是呼叫了一下acquire
和release
方法,所以接下來我們重點來看一下這兩個方法的實現
Node
開始前,先看一下佇列Node的資料結構
static final class Node {
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
其中waitStatus
欄位代表了節點的等待狀態,共包含了5個值。
INIT(0)
: 節點的預設狀態為0。當執行緒釋放資源後,也會將自己的節點狀態設定為0。CANCELLED(1)
: 當前執行緒節點已取消等待,為CANCELLED
的節點會在acquire
方法裡的流程中被清除。SIGNAL(-1)
: 當前節點的後續節點已沉睡,需要被喚醒。會在release
方法裡的流程中將其後續節點喚醒。CONDITION(-2)
和PROPAGATE(-3)
則和條件鎖及共享模式有關,本文不過多解釋。
其中可以看出,狀態可以分為兩類,無效轉態和有效狀態,大於0則代表當前節點無效了,需要被移除,小於等於0則是有效節點,需要繼續去嘗試獲取資源。
acquire
先看一下acquire
方法的實現
public final void acquire(int arg) {
// 呼叫我們自己實現的tryAcquire去獲取資源,獲取失敗則嘗試將自己加入到等待佇列中
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
再看一下addWaiter
方法
private Node addWaiter(Node mode) {
Node node = new Node(mode);
// 為當前執行緒新建一個node,並將其加入到佇列尾部,這裡需要注意的是新增時是先將新節點的prev設定為老的尾部節點,使用CAS將新節點設定為tail後才會將老節點的next設定為新節點,
// 這樣做的理由是為了防止併發問題,後續的對佇列的修改也是包括兩種遍歷,一個是從前往後,一個是從後往前。
for (;;) {
Node oldTail = tail;
if (oldTail != null) {
node.setPrevRelaxed(oldTail);
if (compareAndSetTail(oldTail, node)) {
oldTail.next = node;
return node;
}
} else {
initializeSyncQueue();
}
}
}
新增完節點後,就會嘗試去從佇列中獲取資源,這裡也是AQS的核心了,看一下acquireQueued
方法
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
// 取到當前節點的上一個節點
final Node p = node.predecessor();
// 如果上一個節點是頭節點,則證明前面沒有在等待的執行緒了,輪到當前執行緒去嘗試獲取資源。
if (p == head && tryAcquire(arg)) {
// 獲取資源成功,則將當前節點設為頭節點,並設定為空節點
setHead(node);
p.next = null; // help GC
return interrupted;
}
// 判斷前面一個節點是否是一個有效的等待執行緒節點(沒有取消等待的執行緒),是則將自己睡眠,不是則將前面已取消的執行緒節點移除,直到找到一個有效的執行緒節點作為自己的前序節點並將其狀態設為SIGNAL,然後再走一次迴圈。
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
總結一下acquire
方法的流程:
- 呼叫子類實現的
tryAcquire
方法去獲取資源。 - 獲取成功則當前執行緒繼續執行,否則將自己加入到等待佇列。
- 進入等待佇列後,迴圈的去判斷自己的上一個節點是否是頭結點,是則再次呼叫子類實現的
tryAcquire
方法去獲取資源,否則進入沉睡,等待喚醒。 - 在沉睡前,會將前面已取消的執行緒節點移除,直到找到一個有效的執行緒節點做為自己的前序節點,並將其狀態設為SIGNAL。
一句話就是acquire
方法會去嘗試獲取資源,獲取失敗則將自己加入到等待佇列,並沉睡,等待喚醒。
舉個例子,demo1執行緒獲取資源失敗後,當前的佇列狀態為:
若是有多個執行緒等待,則佇列狀態為:
由圖可以看出,若當前節點已沉睡,則需要在沉睡前將前一個節點的轉態設為SIGNAL
。
release
前面的acquire
方法說到,等待佇列中的執行緒會進入沉睡,等待喚醒,那麼由誰來喚醒呢?答案就是release
方法,也就是我們在呼叫unlock的時候,接下來我們先看一下release
方法的實現
public final boolean release(int arg) {
// 呼叫子類實現的tryRelease方法去釋放資源
if (tryRelease(arg)) {
Node h = head;
// 釋放成功,且等待佇列中還有節點,則去喚醒下一個等待執行緒
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
再看一下unparkSuccessor
方法
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
Node s = node.next;
// 如果下一個節點已經取消了,則從後往前遍歷,找到第一個等待執行緒節點
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
// 找到了等待執行緒節點,並將其喚醒
if (s != null)
LockSupport.unpark(s.thread);
}
喚醒後執行緒將會繼續在acquireQueued
方法裡執行,並嘗試再去獲取資源。
總結一下release
方法的流程:
- 呼叫子類實現的tryRelease方法去釋放資源
- 釋放成功則判斷當前佇列是否有等待執行緒
- 有則找到第一個等待執行緒,並將其喚醒。
從流程上也可以看出,若tryRelease
方法中拋了異常,則會導致所有沉睡的執行緒將無法被喚醒。
AQS總結
AQS是模版設計模式的一種實踐,其將執行緒排程等通用邏輯進行了實現,只預留了tryAcquire
和 tryRelease
方法供子類實現自己的併發工具,大大的減輕了實現的複雜難度。
總結
ReentrantLock 是使用AQS實現的,其主要是實現了AQS的tryAcquire
和 tryRelease
方法,且只需要在lock
和unlock
方法中呼叫一下acquire
和release
方法即可,可以看出AQS非常強大!