碎碎念)
花了兩天時間,終於把ReentrantReadWriteLock(讀寫鎖)解析做完了。之前鑽研過AQS(AbstractQueuedSynchronizer)的原始碼,弄懂讀寫鎖也沒有想象中那麼困難。而且閱讀完ReentrantReadWriteLock的原始碼,正好可以和AQS的原始碼串起來理解,相輔相成。後面博主會盡快把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類物件來實現,對應的原始碼解析也會在後面給出
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記錄其持有的讀鎖數
執行緒區域性計數器
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);
}
可見其預設建立的是非公平讀寫鎖
公平讀寫鎖依賴於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寫執行緒)在同步佇列中等待,則讓步
非公平讀寫鎖
如果要建立非公平讀寫鎖,需要使用有參建構函式,引數fair設定為true:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
非公平讀寫鎖依賴於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讀鎖),都一律選擇讓步
非公平模式:
- 請求寫鎖時,當前執行緒會選擇直接競爭,不會做絲毫的讓步
- 請求讀鎖時,如果發現同步佇列隊首執行緒在等待獲取寫鎖,則會讓步。不過這是一種啟發式演算法,因為寫執行緒可能排在其他讀執行緒後面
如果覺得作者寫的還可以的話,可以?鼓勵一下,嘻嘻