JUC之JDK自帶鎖ReentrantReadWriteLock
一、Hello World!
Java紀年1.5年,ReentrantReadWriteLock誕生於J·U·C。此後,國人一般稱它為讀寫鎖。人如其名,人如其名,她就是一個可重入鎖,同時她還是一個讀、寫鎖。
1.1 跟ReentrantLock並沒有親屬關係
因為ReentrantReadWriteLock在命名上跟ReetrantLock非常貼近,很容易讓人認為她跟ReentrantLock有繼承關係,其實並沒有。ReentrantReadWriteLock實現了ReadWriteLock和Serializable,同時ReadWriteLock跟Lock也沒有繼承關係。
ReentrantReadWriteLock跟ReentrantLock只有朋友關係,她們都是一個可重入鎖。
但是ReentrantReadWriteLock的重入遞迴層級只有65535,即讀鎖能遞迴65535、寫鎖也同樣能夠遞迴65535層。至於為何是65535呢?在AQS框架的時候說過,AQS是用一個Integer來表示鎖的狀態。而一個Integer有32位,讀用一半(16位)、寫用一半(16位),16Bit就是65535。
1.2 對對對,我也有公平性
ReentrantReadWriteLock除與ReentrantLock一樣是具有可重入性之外,她們具有公平性。既是她們都有公平鎖
二、About Me
ReentrantReadWriteLock提供一個讀寫分離的鎖,讀鎖由ReadLock控制,寫鎖由WriteLock完成。當然讀與寫是互斥的。如你所知,可讀不寫,可寫不讀,即是讀寫不能同時進行,這就是讀寫鎖,與眾不同的自我。之所以能做到讀寫互斥說明她們最終還是用了同一個同步器(Sync),對的,她們Forwarding到上層(ReentrantReadWriteLock)的同步器。
我們可想而知,ReentrantReadWriteLock$Sync
// 此處沒有原始碼
其實還是我認為她的原始碼簡潔優雅易懂,大家完成能自行翻閱,大體思路如下:
- 當讀鎖被持有,不管是被一人持有,還是多人持有,寫都需要阻塞。
- 當寫鎖被持有,當然只能有一個人持有了啦,此讀鎖將會被阻塞。
- 讀寫鎖的阻塞方式直接由公平性決定,由FairSync或NonFairSync實現。
- 讀鎖可以有多人同時持有,因此需要記錄持有者數量(HoldCounter)。
呵呵~,關鍵的程式碼後面涉及到了還是會拿出來看的。
2.1 讀鎖
讀鎖是一個共享鎖,即是允許多個執行緒同時獲得同一個鎖。按慣例先看一下原始碼,我們用原來的方式單個單個流程看。
// ReentrantReadWriteLock$ReadLock 原始碼節選
public static class ReadLock implements Lock, java.io.Serializable {
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
}
這裡可以看,ReadLock的功能最終還是由上級的ReentrantReadWriteLock實現的,這點很容易看出來。
這裡的lock/tryLock/unlock
總體的流程與剛剛整理過的ReentrantLock基本一樣,當然之前並沒有實現過共享模式
,不過跟獨佔式差不多。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
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)) {
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 1;
}
return fullTryAcquireShared(current);
}
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()) {
// Make sure we're not acquiring read lock reentrantly
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");
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;
}
}
}
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 {
if (failed)
cancelAcquire(node);
}
}
程式碼依然很長,這個讓我很不開心。
先來看一下fullTryAcquireShared(Thread thread)
,程式碼很長,邏輯很簡單。她返回值如果< 0
表示當前獲取失敗,> 0
表示獲取成功。
- 當前獨佔鎖已經被持有
自已持有了獨佔鎖的話,此時應該是會死鎖;如果不是自己持有返回-1
- 當前獨佔鎖沒有被持有
- 如果需要讀阻塞
確定不是自己的重入,否則會發生死鎖。如果當前沒人持有返回-1
- 如果遞迴層級大於65535
報錯 - 獲得讀鎖
讀鎖持有者的遞迴層級+1
,返回1
- 獲取讀鎖失敗
如果讀鎖失敗回到原點重走一回全流程
- 如果需要讀阻塞
再往上看就是tryAcquireShared(int arg)
同樣返回-1
和1
表示失敗和成功。跟ReentrantLock的獲取鎖方式差不多。
- 第一個
if
判斷的是是否獨佔鎖,是否是自己持有。即判定為是否非重入,非重入則返回-1
- 第二個
if
判斷的是是否有條件嘗試獲取鎖,有條件則嘗試,
- 嘗試成功則需要記錄和統計讀鎖數量,和遞迴層級
- 嘗試失敗則進入上面
fullTryAcquireShared(Thread)
流程
如果上面tryAcquireShared(int arg)
沒有成功獲取到讀鎖的話,則將需要執行doAcquireShared(int arg)
繼續嘗試。
這裡有個for(;;)
,但它並不會使CPU飆升,因為它的實際是有阻塞的,發生在parkAndCheckInterrupt()
由LockSupport.park(this);
完成。這裡就是迴圈嘗試和阻塞直到獲取讀鎖成功。
2.1.1 公平
讀鎖的公平模式是在當前佇列是否有獨佔鎖被持有,若已被有則讀鎖需要阻塞;否則不需要阻塞,因為讀鎖是共享鎖,只與寫鎖互斥。
這就很“公平”了,但其實這種模式的OPS比較低。
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
2.1.2 非公平
人如其名,apparentlyFirstQueuedIsExclusive()
隊頭是不是獨佔鎖。如果是獨佔的話會,讀鎖會一直阻塞。
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
2.2 寫鎖
寫鎖依然是一個排他鎖,即是只允許一個執行緒同時持有這把鎖,也就是獨佔鎖。按國際慣例先看程式碼吧。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 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;
}
首先這裡獲取鎖失敗是指返回false
,寫鎖的獲取流程跟ReentrantLock差不多。這裡需要注意的是一旦執行了 setExclusiveOwnerThread(current)
,之後再想嘗試讀鎖都會進入佇列排隊。
2.2.1 公平
寫鎖的公平模式跟讀鎖的公平模式差不多,非常符合我們的心理預期,所以很好理解。
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
2.2.2 非公平
非公平模式在寫鎖上表現比較明顯,寫鎖有總是能闖入。這個看起來可能有點超出心理預期,如果理解了會覺得更符合邏輯。
final boolean writerShouldBlock() {
return false; // writers can always barge
}
如果是在公平的獲取策略下,其實寫鎖會永遠被阻塞,當持有讀鎖請求的話。即是讀多寫少的情況下,此時是不是寫一直拿不到鎖呢?這是肯定的,所以此時非公平模式能有更高的OPS。
三、See You
在Java 1.8
之前它是JDK實現的讀寫鎖的唯一實現,此後另有高人出山,高人是誰下回再說。
- ReentrantReadWriteLock是ReadWriteLock的唯一實現,在JDK。它由讀、寫鎖組成,讀是共享鎖、寫是獨佔鎖,且讀寫互斥。
- ReentrantLock有很多類似的地方,比如都具有可重入性、都有兩種獲取鎖的策略:公平與非公平,等。
- 與ReentrantLock一樣在非公平模式能獲得更高的OPS。