碎碎念)
花了兩天時間,終於把ReentrantReadWriteLock
(讀寫鎖)解析做完了。之前鑽研過AQS(AbstractQueuedSynchronizer
)的源碼,發現弄懂讀寫鎖也沒有想象中那么困難。而且閱讀完ReentrantReadWriteLock
的源碼,正好可以和AQS的源碼串起來理解,相輔相成
AQS的鏈接貼在下方👇👇👇
全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(一)AQS基礎
全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(二)資源的獲取和釋放
全網最詳細的AbstractQueuedSynchronizer(AQS)源碼剖析(三)條件變量
簡介
ReentrantReadWriteLock
是一個可重入讀寫鎖,內部提供了讀鎖和寫鎖的單獨實現。其中讀鎖用於只讀操作,可被多個線程共享;寫鎖用於寫操作,只能互斥訪問
ReentrantReadWriteLock
尤其適合讀多寫少的應用場景
讀多寫少:
在一些業務場景中,大部分只是讀數據,寫數據很少,如果這種場景下依然使用獨占鎖(如synchronized
),會大大降低性能。因為獨占鎖會使得本該並行執行的讀操作,變成了串行執行
ReentrantReadWriteLock
實現了ReadWriteLock
接口,該接口只有兩個方法,分別用於返回讀鎖和寫鎖,這兩個鎖都是Lock
對象。該接口源碼如下:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReentrantReadWriteLock
有兩個域,分別存放讀鎖和寫鎖:
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
ReentrantReadWriteLock的核心原理主要在於兩點:
- 內部類
Sync
:實現了的AQS大部分方法。Sync
類有兩個子類FairSync
和NonfairSync
,分別實現了公平讀寫鎖和非公平讀寫鎖。Sync
類及其子類的源碼解析會在后面給出 - 內部類
ReadLock
和WriteLock
:分別是讀鎖和寫鎖的具體實現,它們都和ReentrantLock
一樣實現了Lock
接口,因此實現的手段也和ReentrantLock
一樣,都是委托給內部的Sync
類對象來實現,對應的源碼解析也會在后面給出
說什么Sync
類、ReadLock
、WriteLock
類啥的都太抽象,不如一張圖來得實在!ReentrantReadWriteLock
和這些內部類的繼承、聚合關系如下圖所示:

版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任
ReentrantReadWriteLock的特點
讀寫鎖的互斥關系
- 讀鎖和寫鎖之間是互斥關系:當有線程持有讀鎖時,寫鎖不能獲得;當有其他線程持有寫鎖時,讀鎖不能獲得
- 讀鎖和讀鎖之間是共享關系
- 寫鎖和寫鎖之間是互斥關系
可重入性
ReentrantReadWriteLock
在ReadWriteLock
接口之上,添加了可重入的特性,且讀鎖和寫鎖都支持可重入。可重入的含義是:
- 如果一個線程獲取了讀鎖,那么它可以再次獲取讀鎖(但直接獲取寫鎖會失敗,原因見下方的“鎖的升降級”)
- 如果一個線程獲取了寫鎖,那么它可以再次獲取寫鎖或讀鎖
鎖的升降級
鎖升級
ReentrantReadWriteLock
不支持鎖升級,即同一個線程獲取讀鎖后,直接申請寫鎖是不能獲取成功的。測試代碼如下:
public class Test1 {
public static void main(String[] args) {
ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.readLock().lock();
System.out.println("get readLock.");
rtLock.writeLock().lock();
System.out.println("blocking");
}
}
運行到第6行會因為獲取失敗而被阻塞,導致Test1發生死鎖。命令行輸出如下:
get readLock.
鎖降級
ReentrantReadWriteLock
支持鎖降級,即同一個線程獲取寫鎖后,直接申請讀鎖是可以直接成功的。測試代碼如下:
public class Test2 {
public static void main(String[] args) {
ReentrantReadWriteLock rtLock = new ReentrantReadWriteLock();
rtLock.writeLock().lock();
System.out.println("writeLock");
rtLock.readLock().lock();
System.out.println("get read lock");
}
}
該程序不會產生死鎖。結果輸出如下:
writeLock
get read lock
Process finished with exit code 0
讀寫鎖的升降級規則總結
ReentrantReadWriteLock
不支持鎖升級,因為可能有其他線程同時持有讀鎖,而讀寫鎖之間是互斥的,因此升級為寫鎖存在沖突ReentrantReadWriteLock
支持鎖降級,因為如果該線程持有寫鎖時,一定沒有其他線程持有讀鎖或寫鎖,因此降級為讀鎖不存在沖突
公平鎖和非公平鎖
ReentrantReadWriteLock
支持公平模式和非公平模式獲取鎖。從性能上來看,非公平模式更好
二者的規則如下:
- 公平鎖:無論是讀線程還是寫線程,在申請鎖時都會檢查是否有其他線程在同步隊列中等待。如果有,則讓步
- 非公平鎖:如果是讀線程,在申請鎖時會判斷是否有寫線程在同步隊列中等待。如果有,則讓步。不過這是為了防止寫線程餓死,與公平策略無關;如果是寫線程,則直接競爭鎖資源,不會關心有無別的線程正在等待
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任
Sync類
Sync
類是一個抽象類,有兩個具體子類NonfairSync
和FairSync
,分別對應非公平讀寫鎖、公平讀寫鎖。Sync
類的主要作用就是為這兩個子類提供絕絕絕大部分的方法實現
只定義了兩個抽象方法writerShouldBlock
和readerShouldBlocker
交給兩個子類去實現
讀狀態和寫狀態
Sync
類利用AQS單個state
字段,來同時表示讀狀態和寫狀態,源碼如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/*
* Read vs write count extraction constants and functions.
* Lock state is logically divided into two unsigned shorts:
* The lower one representing the exclusive (writer) lock hold count,
* and the upper the shared (reader) hold count.
*/
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// ······
};
根據上面源碼可以看出:
SHARED_SHIFT
表示AQS中的state
(int型,32位)的高16位,作為讀狀態,低16位作為寫狀態SHARED_UNIT
二級制為2^16,讀鎖加1,state
加SHARED_UNIT
MAX_COUNT
就是寫或讀資源的最大數量,為2^16-1- 使用
sharedCount
方法獲取讀狀態,使用exclusiveCount
方法獲取獲取寫狀態
state
划分為讀、寫狀態的示意圖(圖來自網絡)如下,其中讀鎖持有1個,寫鎖持有3個:

記錄首個獲得讀鎖的線程
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
firstReader
記錄首個獲得讀鎖的線程;firstReaderHoldCount
記錄firstReader
持有的讀鎖數
線程局部計數器
Sync
類定義了一個線程局部變量readHolds
,用於保存當前線程重入讀鎖的次數。如果該線程的讀鎖數減為0,則將該變量從線程局部域中移除。相關源碼如下:
// 內部類,用於記錄當前線程重入讀鎖的次數
static final class HoldCounter {
int count = 0;
// 這里使用線程的id而非直接引用,是為了方便GC
final long tid = getThreadId(Thread.currentThread());
}
/* 內部類,繼承ThreadLocal,該類型的變量是每個線程各自保存一份,其中保存的是HoldCounter對象,用set方法保存,get方法獲取 */
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
由於readHolds
變量是線程局部變量(繼承ThreadLocal
類),每個線程都會保存一份副本,不同線程調用其get方法返回的HoldCounter對象不同
readHolds
中的HoldCounter
變量保存了每個讀線程的重入次數,即其持有的讀鎖數量。這么做的目的是便於線程釋放讀鎖時進行合法性判斷:線程在不持有讀鎖的情況下釋放鎖是不合法的,需要拋出IllegalMonitorStateException
異常
緩存
Sync
類定義了一個HoldCounter
變量cachedHoldCounter
,用於保存最近獲取到讀鎖的線程的重入次數。源碼如下:
// 這是一個啟發式算法
private transient HoldCounter cachedHoldCounter;
設計該變量的目的是:將其作為一個緩存,加快代碼執行速度。因為獲取、釋放讀鎖的線程往往都是最近獲取讀鎖的那個線程,雖然每個線程的重入次數都會使用readHolds
來保存,但使用readHolds
變量會涉及到ThreadLocal
內部的查找(lookup),這是存在一定開銷的。有了cachedHoldCounter
這個緩存后,就不用每次都在ThreadLocal
內部查找,加快了代碼執行速度。相當於用空間換時間
獲取鎖
無論是公平鎖還是非公平鎖,它們獲取鎖的邏輯都是相同的,因此Sync
類在這一層就提供了統一的實現
但是,獲取寫鎖和獲取讀鎖的邏輯不相同:
- 寫鎖是互斥資源,獲取寫鎖的邏輯主要在
tryAcquire
方法 - 讀鎖是共享資源,獲取讀鎖的邏輯主要在
tryAcquireShared
方法
具體的源碼分析見下方的“讀鎖”和“寫鎖”各自章節的“獲取x鎖”部分
釋放鎖
無論是公平鎖還是非公平鎖,它們釋放鎖的邏輯都是相同的,因此Sync
類在這一層就提供了統一的實現
但是,釋放寫鎖和釋放讀鎖的邏輯不相同:
- 寫鎖是互斥資源,釋放寫鎖的邏輯主要在
tryRelease
方法 - 讀鎖是共享資源,釋放讀鎖的邏輯主要在
tryReleaseShared
方法
具體的源碼分析見下方的“讀鎖”和“寫鎖”各自章節的“釋放x鎖”部分
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任
寫鎖
寫鎖是由內部類WriteLock
實現的,其實現了Lock
接口,獲取鎖、釋放鎖的邏輯都委托給了sync
域(Sync
對象)來執行。WriteLock
的基本結構如下:
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
// 構造方法注入Sync類對象
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 實現了Lock接口的所有方法
}
獲取寫鎖
WriteLock
使用lock
方法獲取寫鎖,一次獲取一個寫鎖,源碼如下:
public void lock() {
sync.acquire(1);
}
lock
方法內部實際調用的是AQS的acquire
方法,源碼如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
而acquire
方法會調用子類Sync
實現的tryAcquire
方法,如下:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false; // 如果是讀鎖被獲取中,或寫鎖被獲取但不是本線程獲取的,則獲取失敗
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false; // 如果根據公平性判斷此時寫線程需要被阻塞,或在獲取過程中發生競爭且競爭失敗,則獲取失敗
setExclusiveOwnerThread(current);
return true;
}
分為三步:
1、如果讀鎖正在被獲取中,或者寫鎖被獲取中但不是本線程持有,則獲取失敗
2、如果獲取寫鎖達到飽和,則拋出錯誤
3、如果上面兩個都不成立,說明此線程可以請求寫鎖。但需要先根據公平策略來判斷是否應該先阻塞。如果不用阻塞,且CAS成功,則獲取成功。否則獲取失敗
其中公平策略判斷所調用的writerShouldBlock
,在后面分析公平鎖和非公平鎖時會給出分析
如果tryAcquire
方法獲取寫鎖成功,則acquire
方法直接返回,否則進入同步隊列阻塞等待
tryAcquire
體現的讀寫鎖的特征:
- 互斥關系:
- 寫鎖和寫鎖之間是互斥的:如果是別的線程持有寫鎖,那么直接返回false
- 讀鎖和寫鎖之間是互斥的。當有線程持有讀鎖時,寫鎖不能獲得:如果
c!=0
且w==0
,說明此時有線程持有讀鎖,直接返回false
- 可重入性:如果當前線程持有寫鎖,就不用進行公平性判斷
writerShouldBlock
,請求鎖一定會獲取成功 - 不允許鎖升級:如果當前線程持有讀鎖,想要直接申請寫鎖,此時
c!=0
且w==0
,而exclusiveOwnerThread
是null,不等於current
,直接返回false
釋放寫鎖
WriteLock
使用unlock
方法釋放寫鎖,如下:
public void unlock() {
sync.release(1);
}
unlock
內部實際上調用的是AQS的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;
}
而該方法會調用子類Sync
實現的tryAcquire
方法,源碼如下:
protected final boolean tryRelease(int releases) {
// 如果並不持有鎖就釋放,會拋出異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 如果釋放鎖之后鎖空閑,那么需要將鎖持有者置為null
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free; // 返回鎖釋放后是否空閑
}
注意:
任何鎖的釋放都需要判斷是否是在持有鎖的情況下。如果不持有鎖就釋放,會拋出異常。對於寫鎖來說,判斷是否持有鎖很簡單,只需要調用isHeldExclusively
方法進行判斷即可;而對於讀鎖來說,判斷是否持有鎖比較復雜,需要根據每個線程各自保存的持有讀鎖數來判斷,即readHolds
中保存的變量
嘗試獲取寫鎖
WriteLock
使用tryLock
來嘗試獲取寫鎖,如下:
public boolean tryLock( ) {
return sync.tryWriteLock();
}
tryLock
內部實際調用的是Sync
類定義並實現的tryWriteLock
方法。該方法是一個final
方法,不允許子類重寫。其源碼如下:
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1)) // 相比於tryAcquire方法,這里缺少對公平性判斷(writerShouldBlock)
return false;
setExclusiveOwnerThread(current);
return true;
}
其實除了缺少對公平策略判斷writerShouldBlock
的調用以外,和tryAcquire
方法基本上是一樣的,這里不再廢話
Lock接口其他方法的實現
// 支持中斷響應的lock方法,實際上調用的是AQS的acquireInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
// 實際上調用的是AQS的方法tryAcquireNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
// 實際上調用的是Sync類實現的newCondition方法
public Condition newCondition() {
return sync.newCondition();
}
寫鎖支持創建條件變量,因為寫鎖是獨占鎖,而條件變量在await
時會釋放掉所有鎖資源。寫鎖能夠保證所有的鎖資源都是本線程所持有,所以可以放心地去釋放所有的鎖
而讀鎖不支持創建條件變量,因為讀鎖是共享鎖,可能會有其他線程持有讀鎖。如果調用await
,不僅會釋放掉本線程持有的讀鎖,也會釋放掉其他線程持有的讀鎖,這是不被允許的。因此讀鎖不支持條件變量
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任
讀鎖
讀鎖是由內部類ReadLock
實現的,其實現了Lock
接口,獲取鎖、釋放鎖的邏輯都委托給了Sync
類實例sync
來執行。ReadLock
的基本結構如下:
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
// 構造方法注入Sync類對象
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 實現了Lock接口的所有方法
}
獲取讀鎖
ReadLock
使用lock
方法獲取讀鎖,一次獲取一個讀鎖。源碼如下:
public void lock() {
sync.acquireShared(1);
}
lock
方法內部實際調用的是AQS的acquireShared
方法,源碼如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
該方法會調用Sync
類實現的tryAcquireShared
方法,源碼如下:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1; // 如果寫鎖被獲取,且並不是由本線程持有寫鎖,那么獲取失敗
int r = sharedCount(c);
if (!readerShouldBlock() && // 先進行公平性判斷是否應該讓步,這可能會導致重入讀鎖的線程獲取失敗
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { // CAS失敗可能也會導致本能獲取成功的線程獲取失敗
// 如果此時讀鎖沒有被獲取,則該線程是第一個獲取讀鎖的線程,記錄相應信息
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
}
// 該線程不是首個獲取讀鎖的線程,需要記錄到readHolds中
else {
HoldCounter rh = cachedHoldCounter; // 通常當前獲取讀鎖的線程就是最近獲取到讀鎖的線程,所以直接用緩存
// 還是需要判斷一下是不是最近獲取到讀鎖的線程。如果不是,則調用get創建一個新的局部HoldCounter變量
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
// 之前最近獲取讀鎖的線程如果釋放完了讀鎖而導致其局部HoldCounter變量被remove了,這里重新獲取就重新set
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1; // 如果公平性判斷無需讓步,且讀鎖數未飽和,且CAS競爭成功,則說明獲取成功
}
return fullTryAcquireShared(current);
}
tryAcquireShared
的返回值說明:
- 負數:獲取失敗,線程會進入同步隊列阻塞等待
- 0:獲取成功,但是后續以共享模式獲取的線程都不可能獲取成功(這里暫時用不上)
- 正數:獲取成功,且后續以共享模式獲取的線程也可能獲取成功
在讀寫鎖中,tryAcquireShared
沒有返回0的情況,只會返回正數或負數
前面“Sync
類”中講解過這些變量,這里再復習一遍:
firstReader
、firstReaderHoldCount
分別用於記錄第一個獲取到寫鎖的線程及其持有讀鎖的數量cachedHoldCounter
用於記錄最近獲取到寫鎖的線程持有讀鎖的數量readHolds
是一個線程局部變量(ThreadLocal
變量),用於保存每個獲得讀鎖的線程各自持有的讀鎖數量
tryAcquireShared
的流程如下:
1、如果其他線程持有寫鎖,那么獲取失敗(返回-1)
2、否則,根據公平策略判斷是否應該阻塞。如果不用阻塞且讀鎖數量未飽和,則CAS請求讀鎖。如果CAS成功,獲取成功(返回1),並記錄相關信息
3、如果根據公平策略判斷應該阻塞,或者讀鎖數量飽和,或者CAS競爭失敗,那么交給完整版本的獲取方法fullTryAcquireShared
去處理
其中上述步驟2如果發生了重入讀(當前線程持有讀鎖的情況下,再次請求讀鎖),但根據公平策略判斷該線程需要阻塞等待,而導致重入讀失敗。按照正常邏輯,重入讀不應該失敗。不過,tryAcquireShared
並沒有處理這種情況,而是將其放到了fullTryAcquireShared
中進行處理。此外,CAS競爭失敗而導致獲取讀鎖失敗,也交給fullTryAcquireShared
去處理(fullTryAcquireShared
表示我好難-_-)
fullTryAcquireShared
方法是嘗試獲取讀鎖的完全版本,用於處理tryAcquireShared
方法未處理的:
1、CAS競爭失敗
2、因公平策略判斷應該阻塞而導致的重入讀失敗
這兩種情況。其源碼如下:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// 如果當前線程就是firstReader,那么它一定是重入讀,不讓它失敗,而是重新loop直到公平性判斷不阻塞為止
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0) // 當前線程既不持有讀鎖(不是重入讀),並且被公平性判斷為應該阻塞,那么就獲取失敗
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 下面的邏輯基本上和tryAcquire中差不多,不過這里的CAS如果失敗,會重新loop直到成功為止
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
fullTryAcquireShared
其實和tryAcquire
存在很多的冗余之處,但這么做的目的主要是讓tryAcquireShared
變得更簡單,不用處理復雜的CAS循環
fullTryAcquireShared
主要是為了處理CAS失敗和readerShouldBlock
判true而導致的重入讀失敗,這兩種情況在理論上都應該成功獲取鎖。fullTryAcquireShared
的做法就是將這兩種情況放在for
循環中,一旦發生就重新循環,直到成功為止
tryAcquireShared
和fullTryAcquireShared
體現的讀寫鎖特征:
- 互斥關系:
- 讀鎖和讀鎖之間是共享的:即使有其他線程持有了讀鎖,當前線程也能獲取讀鎖
- 讀鎖和寫鎖之間是互斥的。當有其他線程持有寫鎖,讀鎖不能獲得:
tryAcquireShared
第4-6行,fullTryAcquireShared
第5-7行都能體現這一特征
- 可重入性:如果當前線程獲取了讀鎖,那么它再次申請讀鎖一定能成功。這部分邏輯是由
fullTryAcquireShared
的for
循環實現的 - 支持鎖降級:如果當前線程持有寫鎖,那么它申請讀鎖一定會成功。這部分邏輯見
tryAcquireShared
第5行,current
和exclusiveOwnerThread
是相等的,不會返回-1
釋放讀鎖
ReadLock
使用unlock
方法釋放讀鎖,如下:
public void unlock() {
sync.releaseShared(1);
}
unlock
方法實際調用的是AQS的releaseShared
方法,如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
而該方法會調用Sync
類實現的tryReleaseShared
方法,源碼如下:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter; // 一般釋放鎖的都是最后獲取鎖的那個線程
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove(); // 如果釋放讀鎖后不再持有鎖,那么移除readHolds保存的線程局部HoldCounter變量
if (count <= 0)
throw unmatchedUnlockException(); // 拋出IllegalMonitorStateException異常
}
--rh.count;
}
// 循環CAS保證修改state成功
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0; // 如果釋放后鎖空閑,那么返回true,否則返回false
}
}
如果返回true,說明鎖是空閑的,releaseShared
方法會進一步調用doReleaseShared
方法,doReleaseShared
方法會喚醒后繼線程並確保傳播(確保傳播:保證被喚醒的線程可以執行喚醒其后續線程的邏輯)
嘗試釋放讀鎖
ReadLock
使用tryLock
方法嘗試釋放讀鎖,源碼如下:
public boolean tryLock() {
return sync.tryReadLock();
}
tryLock
內部實際調用的是Sync
類定義並實現的tryReadLock
方法。該方法是一個final
方法,不允許子類重寫。其源碼如下:
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
其實除了缺少對公平策略判斷方法readerShouldBlock
的調用以外,和tryAcquireShared
方法基本上是一樣的
Lock接口其他方法的實現
// 支持中斷響應的lock方法,實際上調用的是AQS的acquireSharedInterruptibly方法
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 實際上調用的是AQS的方法tryAcquireSharedNanos方法
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
// 讀鎖不支持創建條件變量
public Condition newCondition() {
throw new UnsupportedOperationException();
}
和寫鎖的區別在於,讀鎖不支持創建條件變量。如果調用newCondition
方法,會直接拋出UnsupportedOperationException
異常。不支持的原因在前面已經分析過,這里不再贅述
版權:本文版權歸作者和博客園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段聲明;必須在文章中給出原文連接;否則 必究法律責任
讀寫鎖的公平策略
ReentrantReadWriteLock
默認構造方法如下:
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
說明其默認創建的是非公平讀寫鎖。如果要創建公平讀寫鎖,需要使用有參構造函數,參數fair設置為true
公平讀寫鎖
公平讀寫鎖依賴於Sync
的子類FairSync
來實現,其源碼如下:
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
writerShouldBlock
writerShouldBlock
實際上調用的是AQS的hasQueuedPredecessors
方法,該方法會檢查是否有線程在同步隊列中等待,源碼如下:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 如果head等於tail,說明是空隊列
// 如果隊首的thread域不是當前線程,說明有別的線程先於當前線程等待獲取鎖
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
writerShouldBlock
只有在tryAcquire
中被調用。如果當前線程請求寫鎖時發現已經有線程(讀線程or寫線程)在同步隊列中等待,則讓步
readerShouldBlock
readerShouldBlock
和writerShouldBlock
一樣,都是調用AQS的hasQueuedPredecessors
方法
readerShouldBlock
只有在tryAcquireShared
(fullTryAcquireShared
)中被調用。如果當前線程請求讀鎖時發現已經有線程(讀線程or寫線程)在同步隊列中等待,則讓步
非公平讀寫鎖
非公平讀寫鎖依賴於Sync
的子類NonfairSync
來實現,其源碼如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
writerShouldBlock
writerShouldBlock
直接返回false
writerShouldBlock
只有在tryAcquire
中被調用,返回false表示在非公平模式下,不管是否有線程在同步隊列中等待,請求寫鎖都不會讓步,而是直接上去競爭
readerShouldBlock
readerShouldBlock
實際調用的是AQS的apparentlyFirstQueuedIsExclusive
方法。其源碼如下:
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
如果同步隊列為空,或隊首線程是讀線程(獲取讀鎖而被阻塞),則返回false。如果同步隊列隊首線程是寫線程(獲取寫鎖而被阻塞),則返回true
readerShouldBlock
只有在tryAcquireShared
(fullTryAcquireShared
)中被調用。如果當前線程請求讀鎖時發現同步隊列隊首線程是寫線程,則讓步。如果是讀線程則跟它爭奪鎖資源
這么做的目的是為了防止寫線程被“餓死”。因為如果一直有讀線程前來請求鎖,且讀鎖是有求必應,就會使得在同步隊列中的寫線程一直不能被喚醒。不過,apparentlyFirstQueuedIsExclusive
只是一種啟發式算法,並不能保證寫線程一定不會被餓死。因為寫線程有可能不在同步隊列隊首,而是排在其他讀線程后面
讀寫鎖的公平策略總結
公平模式:
無論當前線程請求寫鎖還是讀鎖,只要發現此時還有別的線程在同步隊列中等待(寫鎖or讀鎖),都一律選擇讓步
非公平模式:
- 請求寫鎖時,當前線程會選擇直接競爭,不會做絲毫的讓步
- 請求讀鎖時,如果發現同步隊列隊首線程在等待獲取寫鎖,則會讓步。不過這是一種啟發式算法,因為寫線程可能排在其他讀線程后面
如果覺得作者寫的還可以的話,可以👍鼓勵一下