ReentrantReadWriteLock可重入讀寫鎖,同樣是基於AQS實現的。與ReentrantLock區別就是讀寫分離,將鎖的粒度都將細化,提升性能。在共享數據讀寫操作中,讀操作遠遠超過寫操作的次數,那麼可以理解爲共享數據在大部分時間是不變的。synchronized和ReentrantLock作爲互斥鎖,用於這種場景明顯會降低系統的性能。因此,讀寫分離的重入鎖ReentrantReadWriteLock就出現了。
ReentrantLock特點是:讀-讀並行、讀-寫互斥、寫-寫互斥。存在對共享數據修改操作時,自然需要有互斥鎖的特點,但是在不需要修改數據時,可以認爲是無鎖的並行操作。
來個簡單的例子:
public class TestReentrantReadWriteLock {
private Map<String, String> cache = new HashMap<>();
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock();
private Lock writeLock = readWriteLock.writeLock();
public void put(String key, String value) {
writeLock.lock();
try {
cache.put(key, value);
} finally {
wirteLock.unlock();
}
}
public String get(String key) {
readLock.lock();
try {
return cache.getOrDefault(key, "0");
} finally {
readLock.unlock();
}
}
}
分析源碼:
ReentrantReadWriteLock實現ReadWriteLock接口
public interface ReadWriteLock {
//返回讀鎖
Lock readLock();
//返回寫鎖
Lock writeLock();
}
在ReentrantLock中,提供readLock和writeLock的實現方法。構造函數提供支持公平鎖和非公平鎖,默認爲非公平鎖。
//field
//ReentrantLock內部類實現的ReadLock、WriteLock
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
//method
//返回對應的讀寫鎖
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
//構造函數
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
讀鎖(鎖資源共享):
public void lock() {
//sync是基於AQS實現的公平鎖或非公平鎖
sync.acquireShared(1);
}
//AQS提供的獲取讀鎖框架,tryAcquireShared爲具體子類實現的獲取方法
//根據state狀態來識別:鎖是否被持有?是否被讀鎖持有?然後嘗試獲取鎖
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();
//爲什麼寫鎖被非當前線程持有時,纔會返回呢?讀-寫不是互斥嗎?
//假如:某個線程獲取到寫鎖後,業務處理仍然需要讀取某些共享數據時,同線程寫-讀互斥不就完了。
//這行代碼體現了ReentrantReadWriteLock支持鎖降級特性
//return -1,則該節點將在AQS獲取共享鎖框架中入隊,如果是head後繼則會再次嘗試
//調用acquiredShared獲取鎖;非head後繼則嘗試park
//注意:1、ReentrantReadWriteLock不支持鎖升級(擁有讀鎖申請寫鎖)
// 2、在鎖升級後,釋放寫鎖,其他線程讀鎖線程就可以獲取讀鎖
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
//readerShouldBlock()在公平鎖和非公平鎖中提供的策略是不一樣的。
//非公平鎖中,head後繼節點是EXCLUSIVE類型的,說明存在寫鎖獲取鎖,在讀寫鎖中且非公平鎖條件下,不阻塞讀鎖,會導致寫鎖飢餓,除過這種場景外,其他場景都不會阻塞讀鎖獲取
//公平鎖中,如果雙向鏈表爲空、(非空&&head後繼==null)、(非空&&head後繼是當前線程)時,return false,可以嘗試獲取鎖資源
//SHARED_UNIT爲65536,讀寫鎖是將int state屬性值得高16位作爲讀鎖,低16位作爲寫鎖
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r==0表示讀鎖爲0,沒有線程獲取讀鎖
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//r!=0且firstReader==current說明firstReaderHoldCount是自己重入次數
} else if (firstReader == current) {
firstReaderHoldCount++;
//r!=0且firstReader!=current則需要獲取自身的重入次數
} else {
HoldCounter rh = cachedHoldCounter;
//rh!=null&&rh.tid跟cachedHoldeCounter不一樣說明上個讀鎖線程不是自己,則通過ThreadLocal獲取自己的重入次數
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
//rh!=null&&rh.tid==current,說明就是自己了
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();
//持有寫鎖的線程不是當前線程,返回-1.後續加入雙向鏈表中
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
//沒有互斥寫鎖,但是需要根據公平或非公平鎖判斷是否需要阻塞
//1、是公平鎖時,前置節點非當前線程,但是正在執行的線程是當前線程,可以嘗試獲取
//2、非公平鎖時,當head後繼節點是互斥鎖時,需要阻塞,但是正在執行的線程是當前線程,則可以嘗試獲取鎖。如:寫操作時,需要讀鎖獲取某個值,拒絕讀鎖獲取數據豈不是就死鎖了
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
//當讀鎖不是第一次獲取讀時,需要嘗試獲取讀鎖。可重入。否則就會容易引起死鎖。而如果是第一次嘗試獲取讀鎖的話,直接退出return -1,進入AQS等待鏈表
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");
//嘗試獲取讀鎖,並return 1
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;
}
}
}
從上述代碼可以看出,不管是否是公平鎖,有一個問題是例外。如果當前正在執行的已擁有讀鎖或寫鎖的線程再次獲取讀鎖時,都需要循環獲取讀鎖,否則讀鎖一直獲取不到讀鎖加入AQS鏈表中,那麼該線程將會阻塞,嚴重影響性能。
private void doAcquireShared(int arg) {
//將node節點加入AQS鏈表
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//線程沒有被阻塞或沒有獲取到鎖,一直循環
for (;;) {
//加入到鏈表後,前驅接點是head,說明它是第一個元素,則嘗試獲取鎖
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);
}
}
讀鎖到這塊差不多結束了,看得頭昏腦漲的……
寫鎖結構跟ReentrantLock完全一樣啊,當然tryAcquire需要子類自己實現了。首先嚐試獲取鎖,獲取失敗進入鏈表,再判斷是否爲head後繼,如果是則再次tryAcquire嘗試獲取鎖,如果不是則看前繼節點是否能安全的park,並檢測中斷屬性值。
//寫鎖結構跟ReentrantLock完全一樣,只需要關注tryAcquire(arg)即可
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//鎖的狀態
int c = getState();
//互斥鎖的狀態
int w = exclusiveCount(c);
//state!=0說明,寫鎖前有讀鎖或寫鎖申請了鎖資源
if (c != 0) {
//有鎖,w==0說明,上次申請鎖的是讀鎖;current!=線程持有鎖不是當前線程
//線程持有鎖只會是寫鎖設置的
if (w == 0 || current != getExclusiveOwnerThread())
//獲取失敗時,又到了進入雙向鏈表,是否爲head的後繼嘗試獲取鎖的流程
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//w!=0&¤t==getExclusveOwnerThread就是寫鎖重入了
setState(c + acquires);
return true;
}
//此處state==0,沒有任何線程持有鎖
//writerShouldBlock是公平鎖和非公平鎖都實現的方法
//非公平鎖直接返回false,公平鎖則判斷空鏈表或head後繼是當前線程返回false;
//cas設置state值
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//設置當前線程獨佔鎖
setExclusiveOwnerThread(current);
return true;
}
Condition:
只有寫鎖支持Condition,讀鎖會拋出UnsupportedOperationException異常,讀鎖不支持Condition的原因,個人理解跟讀寫鎖的應用場景有關,支持高併發的讀。讀之間都要支持Condition的話,讀寫鎖實現會非常複雜,而且又丟了讀寫鎖的應用場景。
讀鎖和寫鎖釋放鎖的邏輯就不細讀了,大致就是:一直嘗試釋放鎖,然後喚醒head後繼節點,接着讓它嘗試獲取鎖的過程。
未完待續,去玩會再說……