AQS(AbstractQueuedSynchronizer)佇列同步器,是JUC中非常重要的一個元件,基於它可以簡單高效地構建一些通用的鎖和同步器,如ReentrantLock、Semaphore等(本文學習內容基於JDK1.8),本文主要關注AQS的原始碼實現及基於AQS實現的一些常用的同步元件
基本內容
通過使用JUC中的同步元件,可以比較簡潔地進行併發程式設計,而在很多同步元件的實現中都出現了Sync extends AbstractQueuedSynchronizer
的身影,通過對AQS的一些方法的重寫,實現了相應的元件的功能。AQS是實現鎖的關鍵,其中鎖是面向鎖的使用者的,定義了鎖的使用方式,而AQS是面向鎖的實現者的,簡化了鎖的實現方式,遮蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。
AQS採用了模板方法設計模式,支援通過子類重寫相應的方法實現不同的同步器。在AQS中,有一個state變數,表示同步狀態(這裡的同步狀態就可以看作是一種資源,對同步狀態的獲取可以看作是對同步資源的競爭),AQS提供了多種獲取同步狀態的方式,包括獨佔式獲取、共享式獲取以及超時獲取等,下面會進行具體的介紹。
原理分析
下面將結合原始碼從模板方法、同步狀態管理、CLH鎖佇列、獨佔式獲取方式、共享式獲取方式、超時獲取方式等方面分析AQS的原理及實現
模板方法
可以通過子類重寫的方法列表如下
方法名稱 | 用途 |
---|---|
tryAcquire(int arg) | 主要用於實現獨佔式獲取同步狀態,實現該方法需要查詢當前狀態是否符合預期,然後進行相應的狀態更新實現控制(獲取成功返回true,否則返回false,成功通常是可以更新同步狀態,失敗則是不符合更新同步狀態的條件),其中arg表示需要獲取的同步狀態數 |
tryRelease(int arg) | 主要用於實現獨佔式釋放同步狀態,同時更新同步狀態(通常在同步狀態state更新為0才會返回true,表示已經徹底釋放同步資源),其中arg表示需要釋放的同步狀態數 |
tryAcquireShared(int arg) | 主要用於實現共享式獲取同步狀態,同時更新同步狀態 |
tryReleaseShared(int arg) | 主要用於實現共享式釋放同步狀態,同時更新同步狀態 |
isHeldExclusively() | 一般用於判斷同步器是否被當前執行緒獨佔 |
同步狀態管理
對執行緒進行加鎖在AQS中體現為對同步狀態的操作,通過的同步狀態地管理,可以實現不同的同步任務,同步狀態state
是AQS很關鍵的一個域
// 因為state是volatile的,所以get、set方法均為原子操作,而compareAndSetState方法
// 使用了Unsafe類的CAS操作,所以也是原子的
// 同步狀態
private volatile int state;
// 同步狀態的操作包括
// 獲取同步狀態
protected final int getState() { return state;}
// 設定同步狀態
protected final void setState(int newState) { state = newState;}
// CAS操作更新同步狀態
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
複製程式碼
CLH鎖佇列
CLH(Craig, Landin, and Hagersten)鎖,是自旋鎖的一種。AQS中使用了CLH鎖的一個變種,實現了一個雙向佇列,並使用其實現阻塞的功能,通過將請求共享資源的執行緒封裝為佇列中的一個結點實現鎖的分配。
雙向佇列的頭結點記錄工作狀態下的執行緒,後繼結點若獲取不了同步狀態則會進入阻塞狀態,新的結點會從隊尾加入佇列,競爭同步狀態
// 佇列的資料結構如下
// 結點的資料結構
static final class Node {
// 表示該節點等待模式為共享式,通常記錄於nextWaiter,
// 通過判斷nextWaiter的值可以判斷當前結點是否處於共享模式
static final Node SHARED = new Node();
// 表示節點處於獨佔式模式,與SHARED相對
static final Node EXCLUSIVE = null;
// waitStatus的不同狀態,具體內容見下文的表格
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;
// 用於記錄共享模式(SHARED), 也可以用來記錄CONDITION佇列(見擴充套件分析)
Node nextWaiter;
// 通過nextWaiter的記錄值判斷當前結點的模式是否為共享模式
final boolean isShared() { return nextWaiter == SHARED;}
// 獲取當前結點的前置結點
final Node predecessor() throws NullPointerException { ... }
// 用於初始化時建立head結點或者建立SHARED結點
Node() {}
// 在addWaiter方法中使用,用於建立一個新的結點
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
// 在CONDITION佇列中使用該建構函式新建結點
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
// 記錄頭結點
private transient volatile Node head;
// 記錄尾結點
private transient volatile Node tail;
複製程式碼
Node狀態表(waitStatus,初始化時預設為0)
狀態名稱 | 狀態值 | 狀態描述 |
---|---|---|
CANCELLED | 1 | 說明當前結點(即相應的執行緒)是因為超時或者中斷取消的,進入該狀態後將無法恢復 |
SIGNAL | -1 | 說明當前結點的後繼結點是(或者將要)由park導致阻塞的,當結點被釋放或者取消時,需要通過unpark喚醒後繼結點(表現為unparkSuccessor()方法) |
CONDITION | -2 | 該狀態是用於condition佇列結點的,表明結點在等待佇列中,結點執行緒等待在Condition上,當其他執行緒對Condition呼叫了signal()方法時,會將其加入到同步佇列中去,關於這一部分的內容會在擴充套件中提及。 |
PROPAGATE | -3 | 說明下一次共享式同步狀態的獲取將會無條件地向後繼結點傳播 |
下圖展示該佇列的基本結構
獨佔式獲取方式
獨佔式(EXCLUSIVE)獲取需重寫tryAcquire
、tryRelease
方法,並訪問acquire
、release
方法實現相應的功能。
acquire的流程圖如下:
上述流程圖比較複雜,這裡簡單概述一下其中的過程
- 執行緒嘗試獲取同步狀態,如果成功獲取則繼續執行,如果獲取失敗則加入到同步佇列自旋
- 自旋過程中若立即獲取到同步狀態(前置結點為head並且嘗試獲取同步狀態成功)則可以直接執行
- 若無法立即獲取到同步狀態則會將前置結點置為SIGNAL狀態同時自身通過park()方法進入阻塞狀態,等待unpark()方法喚醒
- 若執行緒被unpark()方法(此時說明前置結點在執行release操作)喚醒後,前置結點是頭結點並且被喚醒的執行緒獲取到了同步狀態,則恢復工作
主要程式碼如下:
// 這裡不去看tryAcquire、tryRelease方法的具體實現,只知道它們的作用分別為嘗試獲取同步狀態、
// 嘗試釋放同步狀態
public final void acquire(int arg) {
// 如果執行緒直接獲取成功,或者再嘗試獲取成功後都是直接工作,
// 如果是從阻塞狀態中喚醒開始工作的執行緒,將當前的執行緒中斷
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 包裝執行緒,新建結點並加入到同步佇列中
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 嘗試入隊, 成功返回
if (pred != null) {
node.prev = pred;
// CAS操作設定隊尾
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 通過CAS操作自旋完成node入隊操作
enq(node);
return node;
}
// 在同步佇列中等待獲取同步狀態
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor();
// 檢查是否符合開始工作的條件
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
// 獲取不到同步狀態,將前置結點標為SIGNAL狀態並且通過park操作將node包裝的執行緒阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果獲取失敗,將node標記為CANCELLED
if (failed)
cancelAcquire(node);
}
}
複製程式碼
release流程圖如下
release的過程比較簡單,主要就是通過tryRelease更新同步狀態,然後如果需要,喚醒後置結點中被阻塞的執行緒
主要程式碼如下
// release
public final boolean release(int arg) {
// 首先嚐試釋放並更新同步狀態
if (tryRelease(arg)) {
Node h = head;
// 檢查是否需要喚醒後置結點
if (h != null && h.waitStatus != 0)
// 喚醒後置結點
unparkSuccessor(h);
return true;
}
return false;
}
// 喚醒後置結點
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 通過CAS操作將waitStatus更新為0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
// 檢查後置結點,若為空或者狀態為CANCELLED,找到後置非CANCELLED結點
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 喚醒後置結點
if (s != null)
LockSupport.unpark(s.thread);
}
複製程式碼
共享式獲取方式
共享式(SHARED)獲取需重寫tryAcquireShared
、tryReleaseShared
方法,並訪問acquireShared
、releaseShared
方法實現相應的功能。與獨佔式相對,共享式支援多個執行緒同時獲取到同步狀態並進行工作
acquireShared
acquireShared過程和acquire非常相似,流程大致相同,下面簡單概括一下
- 執行緒獲取同步狀態,若能獲取到,則直接執行,如獲取不到,新建共享式結點進入同步佇列
- 由於獲取不到同步狀態,執行緒將被park方法阻塞,等待被喚醒
- 被喚醒後,滿足獲取同步狀態的條件,會向後傳播,喚醒後繼結點
//
public final void acquireShared(int arg) {
// 嘗試共享式獲取同步狀態,如果成功獲取則可以繼續執行,否則執行doAcquireShared
if (tryAcquireShared(arg) < 0)
// 以共享式不停得嘗試獲取同步狀態
doAcquireShared(arg);
}
// Acquires in shared uninterruptible mode.
private void doAcquireShared(int arg) {
// 向同步佇列中新增一個共享式的結點
final Node node = addWaiter(Node.SHARED);
// 標記獲取失敗狀態
boolean failed = true;
try {
// 標記中斷狀態(若在該過程中被中斷是不會響應的,需要手動中斷)
boolean interrupted = false;
// 自旋
for (;;) {
// 獲取前置結點
final Node p = node.predecessor();
// 若前置結點為頭結點
if (p == head) {
// 嘗試獲取同步狀態
int r = tryAcquireShared(arg);
// 若獲取到同步狀態。
if (r >= 0) {
// 此時,當前結點儲存的執行緒恢復執行,需要將當前結點設定為頭結點並且向後傳播,
// 通知符合喚醒條件的結點一起恢復執行
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 需要中斷,中斷當前執行緒
if (interrupted)
selfInterrupt();
// 獲取成功
failed = false;
return;
}
}
// 獲取同步狀態失敗,需要進入阻塞狀態
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 獲取失敗,CANCELL node
if (failed)
cancelAcquire(node);
}
}
// 將node設定為同步佇列的頭結點,並且向後通知當前結點的後置結點,完成傳播
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
// 向後傳播
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if(s == null || s.isShared())
doReleaseShared();
}
}
複製程式碼
releasShared
releaseShared在嘗試釋放同步狀態成功後,會喚醒後置結點,並且保證傳播性
public final boolean releaseShared(int arg) {
// 嘗試釋放同步狀態
if (tryReleaseShared(arg)) {
// 成功後喚醒後置結點
doReleaseShared();
return true;
}
return false;
}
// 喚醒後置結點
private void doReleaseShared() {
// 迴圈的目的是為了防止新結點在該過程中進入同步佇列產生的影響,同時要保證CAS操作的完成
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
複製程式碼
超時獲取方式
超時獲取使通過AQS實現的鎖支援超時獲取鎖,這是synchronized關鍵字所不具備的,關於其具體的實現,和上述實現方式相似,只是在獨佔式、共享式獲取的基礎上增加了時間的約束,同時通過parkNanos()方法為阻塞定時,這裡不再過多展開。
例項分析
下面列舉幾個常用的併發元件
ReetrantLock
ReentrantLock,重入鎖。通過AQS獨佔式實現加鎖、解鎖操作,支援同一執行緒重複獲取鎖。主要操作為lock,unlock,其實現分別依賴acquire和release
private final Sync sync;
// 繼承AQS,重寫相應方法
abstract static class Sync extends AbstractQueuedSynchronizer {
abstract void lock();
final boolean nonfairTryAcquire(int acquires) { ... }
protected final boolean tryRelease(int releases) { ... }
// ...略
}
static final class NonfairSync extends Sync {
final void lock() { ... }
protected final boolean tryAcquire(int acquires) { ... }
}
static final class FairSync extends Sync {
final void lock() { ... }
protected final boolean tryAcquire(int acquires) { ... }
}
複製程式碼
相關總結
- 重入鎖支援公平獲取、非公平(預設)獲取兩種方式,通過建構函式fair來決定用NonfairSync還是用FairSync完成sync的例項化,兩種方式的區別在於公平式要求鎖的獲取順序應該符合申請的時間順序,即嚴格按照同步佇列FIFO,而非公平式則不考慮(公平式通過對當前結點的前置結點進行判斷來保證公平性)
- 重入鎖的加鎖邏輯是,若鎖尚未被獲取(state = 0),說明可以直接獲取到鎖並且更新同步狀態(此時需要CAS更新保證原子性),若鎖已經被獲取,判斷獲取鎖的執行緒是否為當前執行緒,若是,則更新同步狀態(state + acquires,此時直接更新即可,因為只有該執行緒可以訪問該段程式碼),說明同樣可以獲取到鎖。否則,當前獲取不到鎖,執行緒會被阻塞
- 重入鎖的解鎖邏輯是,更新同步狀態(state - releases),若state為0,說明該執行緒完全釋放鎖,返回true,否則返回false
CountDownLatch
CountDownLatch,一種同步工具,可以使一個或多個執行緒一直等待,直到指定數量執行緒全部執行完畢後才再執行。通過AQS共享式實現。主要操作為await(讓當前執行緒等待,呼叫了AQS的acquireSharedInterruptibly方法,可以簡單將其當作acquireShared方法,實現基本相同),countDown(執行同步狀態釋放的操作),其實大體的思路就是,初始化CountDownLatch一定的同步狀態數,執行await操作的執行緒需等待同步狀態數完全釋放(為0)時才可以執行,而需要先完成的任務在完成後都通過countDown釋放一定的同步狀態數
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
// 檢查同步狀態數是否已經為0,不為0則同步狀態獲取失敗
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 釋放一定的同步狀態數
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
private final Sync sync;
複製程式碼
ReentrantReadWriteLock
ReentrantReadWriteLock,可重入的讀寫鎖,同時使用了AQS的獨佔式和共享式,當進行寫操作時,鎖由寫執行緒獨佔,其他寫執行緒和讀執行緒阻塞。當進行讀操作時,寫執行緒阻塞,所有讀執行緒可以共享鎖。讀寫鎖的實現相對複雜,這裡不再貼過多的程式碼,簡單概括一下其實現的方式:
- 讀寫鎖內部是通過一個讀鎖、一個寫鎖實現的。而讀鎖和寫鎖共享一個同步狀態state,那麼讀寫狀態的就由同步狀態決定。讀寫鎖採取按位分割的方法實現一個同步狀態表示兩種不同型別(讀和寫)的狀態的。讀寫鎖將變數分為兩部分,高16位表示讀,低16位表示寫。那麼寫狀態的值就為state&0x0000ffff,對其修改操作可以直接對state進行。讀狀態的值為state>>16,對其修改操作(如加操作)為(state+0x00010000)
- 寫鎖的獲取邏輯,如果當前執行緒已經獲取了寫鎖,則增加寫狀態;如果讀鎖已經被獲取或者獲取寫鎖的執行緒不為當前執行緒,則當前執行緒進入同步佇列中等待。如果還沒有鎖獲取執行緒,則直接獲取。
- 寫鎖的釋放邏輯,減少寫狀態,直至寫狀態為0表示寫鎖完全被釋放
- 讀鎖的獲取邏輯,寫鎖未被獲取時,讀鎖總可以被獲取。若當前執行緒已經獲取了讀鎖,則增加讀狀態(為各讀執行緒的讀狀態之和,各執行緒的讀狀態記錄在ThreadLocal中)。若寫鎖已經被獲取,則無法獲取讀鎖。
- 讀鎖的釋放邏輯,每次釋放都會減少讀狀態
- ReentrantReadWriteLock支援鎖降級,指的是獲取寫鎖後,先獲取讀鎖然後再釋放寫鎖,完成寫鎖到讀鎖的降級。這樣可以保證資料可見性,防止寫鎖直接釋放後,其他執行緒獲取了寫鎖,則當前執行緒可能無法獲取寫鎖執行緒的修改。但是讀寫鎖不支援鎖升級,原因相同。
擴充套件
Condition
在synchronized加鎖的時候,可以通過Object類的方法的wait()、notify()方法實現等待通知,那麼在Lock鎖的過程中,也存在類似的操作,即Condition介面,該介面提供了await()、signal()方法,具有相同的功能。
在AQS中,有一個類ConditionObject,實現了Condition介面。它同樣使用了Node的資料結構,構成了一個佇列(FIFO),與同步佇列區別,可以叫它等待佇列。獲取Condition需要通過Lock介面的newCondition方法,這意味著一個Lock可以有多個等待佇列,而Object監視器模型提供的一個物件僅有一個等待佇列
// Condition的資料結構
static final class Node {
// next 指標
Node nextWaiter;
// ...
}
public class ConditionObject implements Condition, java.io.Serializable {
// head
private transient Node firstWaiter;
// tail
private transient Node lastWaiter;
// ...
}
複製程式碼
下面具體來看await和signal操作
await()
// 涉及中斷的操作,暫時忽略
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 向等待佇列的隊尾新建一個CONDITION結點
Node node = addConditionWaiter();
// 因為要進入等待狀態,所以需要釋放同步狀態(即釋放鎖),如果失敗,該結點會被CANCELLED
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判讀該結點是否在同步佇列上,如果不在就通過park操作將其阻塞,進入等待狀態
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 從等待狀態恢復,進入同步佇列競爭同步狀態
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 向等待佇列的隊尾新建一個CONDITION結點
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果最後一個結點的waitStatus並非CONDITION,說明該結點被CANCELLED了,需要
// 從佇列中清除掉
if (t != null && t.waitStatus != Node.CONDITION) {
// 將CANCELLED結點從等待佇列中清除出去
unlinkCancelledWaiters();
t = lastWaiter;
}
// 新建CONDITION結點並且將其加入隊尾
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
複製程式碼
關於await的操作
- 執行await操作後,執行緒會被包裝成CONDITION結點進入等待佇列
- 通過park使執行緒阻塞
- 被喚醒後,執行緒從等待佇列進入同步佇列競爭同步狀態
signal()
//
public final void signal() {
// 校驗
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 喚醒等待佇列的頭結點
if (first != null)
doSignal(first);
}
// 執行喚醒操作
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 喚醒結點並且將其加入同步佇列
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 將喚醒的結點加入到同步佇列中競爭同步狀態,恢復執行
final boolean transferForSignal(Node node) {
// 將node的狀態從CONDITION恢復到預設狀態,該CAS操作由外層doSignal的迴圈保證成功操作
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 將node加入到同步佇列中
Node p = enq(node);
int ws = p.waitStatus;
// 如果前置結點已經被取消或者將前置結點設定為SIGNAL失敗,就通過unpark喚醒node包裝的執行緒
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
複製程式碼
關於signal的操作
將等待佇列的頭結點喚醒,從等待佇列中移除,並將其加入到同步佇列中競爭同步狀態,恢復執行
還有一些操作,如signalAll()則是將等待佇列中的全部結點從等待佇列中移除並加入到同步佇列中競爭同步狀態
StampedLock
StampedLock是Java8中新增的一個鎖,是對讀寫鎖的改進。讀寫鎖雖然分離了讀與寫的功能,但是它在處理讀與寫的併發上,採取的是一種悲觀的策略,這就導致了,當讀取的情況很多而寫入的情況很少時,寫入執行緒可能遲遲無法競爭到鎖並被阻塞,遭遇飢餓問題。
StampedLock提供了3種控制鎖的模式,寫、讀、樂觀讀。在加鎖時可以獲取一個stamp作為校驗的憑證,在釋放鎖的時候需要校驗這個憑證,如果憑證失效的話(比如在讀的過程中,寫執行緒產生了修改),就需要重新獲取憑證,並且重新獲取資料。這很適合在寫入操作較少,讀取操作較多的情景,可以樂觀地認為寫入操作不會發生在讀取資料的過程中,而是在讀取執行緒解鎖前進行憑證的校驗,在必要的情況下,切換成悲觀讀鎖,完成資料的獲取。這樣可以大幅度提高程式的吞吐量。
StampedLock在實現上沒有藉助AQS,但是其很多設計的思想、方法都是參照AQS並進行了一些修改完成的。在StampedLock內部同樣維護了一個CLH佇列完成相關的功能。
與ReentrantReadWriteLock相比,StamptedLock的API呼叫相對複雜一些,所以在很多時候還是會用ReentrantReadWriteLock。
更多的關於StampedLock的內容後續再補充。
參考資料
如有問題,還請指出