ReadWriteLock管理一組鎖,一個是只讀的鎖,一個是寫鎖。讀鎖可以在沒有寫鎖的時候被多個線程同時持有,寫鎖是獨占的。
所有讀寫鎖的實現必須確保寫操作對讀操作的內存影響。換句話說,一個獲得了讀鎖的線程必須能看到前一個釋放的寫鎖所更新的內容。
讀寫鎖比互斥鎖允許對于共享數據更大程度的并發(fā)。每次只能有一個寫線程,但是同時可以有多個線程并發(fā)地讀數據。ReadWriteLock適用于讀多寫少的并發(fā)情況。
Java并發(fā)包中ReadWriteLock是一個接口,主要有兩個方法,如下:
public interface ReadWriteLock {
/**
* 返回讀鎖
*/
Lock readLock();
/**
* 返回寫鎖
*/
Lock writeLock();
}
Java并發(fā)庫中ReetrantReadWriteLock實現了ReadWriteLock接口并添加了可重入的特性。
ReentrantReadWriteLock分析
特性
ReentrantReadWriteLock有如下特性:
- 獲取順序
- 非公平模式(默認)
當以非公平初始化時,讀鎖和寫鎖的獲取的順序是不確定的。非公平鎖主張競爭獲取,可能會延緩一個或多個讀或寫線程,但是會比公平鎖有更高的吞吐量。 - 公平模式
當以公平模式初始化時,線程將會以隊列的順序獲取鎖。當當前線程釋放鎖后,等待時間最長的寫鎖線程就會被分配寫鎖;或者有一組讀線程組等待時間比寫線程長,那么這組讀線程組將會被分配讀鎖。
當有寫線程持有寫鎖或者有等待的寫線程時,一個嘗試獲取公平的讀鎖(非重入)的線程就會阻塞。這個線程直到等待時間最長的寫鎖獲得鎖后并釋放掉鎖后才能獲取到讀鎖。
- 非公平模式(默認)
- 可重入
允許讀鎖可寫鎖可重入。寫鎖可以獲得讀鎖,讀鎖不能獲得寫鎖。 - 鎖降級
允許寫鎖降低為讀鎖 - 中斷鎖的獲取
在讀鎖和寫鎖的獲取過程中支持中斷 - 支持Condition
寫鎖提供Condition實現 - 監(jiān)控
提供確定鎖是否被持有等輔助方法
使用
下面一段代碼展示了鎖降低的操作:
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
ReentrantReadWriteLock可以用來提高某些集合的并發(fā)性能。當集合比較大,并且讀比寫頻繁時,可以使用該類。下面是TreeMap使用ReentrantReadWriteLock進行封裝成并發(fā)性能提高的一個例子:
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
源碼分析
構造方法
ReentrantReadWriteLock有兩個構造方法,如下:
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
可以看到,默認的構造方法使用的是非公平模式,創(chuàng)建的Sync是NonfairSync對象,然后初始化讀鎖和寫鎖。一旦初始化后,ReadWriteLock接口中的兩個方法就有返回值了,如下:
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
從上面可以看到,構造方法決定了Sync是FairSync還是NonfairSync。Sync繼承了AbstractQueuedSynchronizer,而Sync是一個抽象類,NonfairSync和FairSync繼承了Sync,并重寫了其中的抽象方法。
Sync分析
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和readerShouldBlock方法都表示當有別的線程也在嘗試獲取鎖時,是否應該阻塞。
對于公平模式,hasQueuedPredecessors()方法表示前面是否有等待線程。一旦前面有等待線程,那么為了遵循公平,當前線程也就應該被掛起。
下面再來看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() {
/* 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();
}
}
從上面可以看到,非公平模式下,writerShouldBlock直接返回false,說明不需要阻塞;而readShouldBlock調用了apparentFirstQueuedIsExcluisve()方法。該方法在當前線程是寫鎖占用的線程時,返回true;否則返回false。也就說明,如果當前有一個寫線程正在寫,那么該讀線程應該阻塞。
繼承AQS的類都需要使用state變量代表某種資源,ReentrantReadWriteLock中的state代表了讀鎖的數量和寫鎖的持有與否,整個結構如下:
可以看到state的高16位代表讀鎖的個數;低16位代表寫鎖的狀態(tài)。
獲取鎖
讀鎖的獲取
當需要使用讀鎖時,首先調用lock方法,如下:
public void lock() {
sync.acquireShared(1);
}
從代碼可以看到,讀鎖使用的是AQS的共享模式,AQS的acquireShared方法如下:
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
當tryAcquireShared()方法小于0時,那么會執(zhí)行doAcquireShared方法將該線程加入到等待隊列中。
Sync實現了tryAcquireShared方法,如下:
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
//如果當前有寫線程并且本線程不是寫線程,不符合重入,失敗
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//得到讀鎖的個數
int r = sharedCount(c);
//如果讀不應該阻塞并且讀鎖的個數小于最大值65535,并且可以成功更新狀態(tài)值,成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//如果當前讀鎖為0
if (r == 0) {
//第一個讀線程就是當前線程
firstReader = current;
firstReaderHoldCount = 1;
}
//如果當前線程重入了,記錄firstReaderHoldCount
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;
}
//否則,循環(huán)嘗試
return fullTryAcquireShared(current);
}
從上面的代碼以及注釋可以看到,分為三步:
- 如果當前有寫線程并且本線程不是寫線程,那么失敗,返回-1
- 否則,說明當前沒有寫線程或者本線程就是寫線程(可重入),接下來判斷是否應該讀線程阻塞并且讀鎖的個數是否小于最小值,并且CAS成功使讀鎖+1,成功,返回1。其余的操作主要是用于計數的
- 如果2中失敗了,失敗的原因有三,第一是應該讀線程應該阻塞;第二是因為讀鎖達到了上線;第三是因為CAS失敗,有其他線程在并發(fā)更新state,那么會調動fullTryAcquireShared方法。
fullTryAcquiredShared方法如下:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//一旦有別的線程獲得了寫鎖,返回-1,失敗
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
//如果讀線程需要阻塞
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");
//如果成功更改狀態(tài),成功返回
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與tryAcquireShared有很多類似的地方。
在上面可以看到多次調用了readerShouldBlock方法,對于公平鎖,只要隊列中有線程在等待,那么將會返回true,也就意味著讀線程需要阻塞;對于非公平鎖,如果當前有線程獲取了寫鎖,則返回true。一旦不阻塞,那么讀線程將會有機會獲得讀鎖。
寫鎖的獲取
寫鎖的lock方法如下:
public void lock() {
sync.acquire(1);
}
AQS的acquire方法如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
從上面可以看到,寫鎖使用的是AQS的獨占模式。首先嘗試獲取鎖,如果獲取失敗,那么將會把該線程加入到等待隊列中。
Sync實現了tryAcquire方法用于嘗試獲取一把鎖,如下:
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.
*/
//得到調用lock方法的當前線程
Thread current = Thread.currentThread();
int c = getState();
//得到寫鎖的個數
int w = exclusiveCount(c);
//如果當前有寫鎖或者讀鎖
if (c != 0) {
// 如果寫鎖為0或者當前線程不是獨占線程(不符合重入),返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//如果寫鎖的個數超過了最大值,拋出異常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 寫鎖重入,返回true
setState(c + acquires);
return true;
}
//如果當前沒有寫鎖或者讀鎖,如果寫線程應該阻塞或者CAS失敗,返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//否則將當前線程置為獲得寫鎖的線程,返回true
setExclusiveOwnerThread(current);
return true;
}
從代碼和注釋可以看到,獲取寫鎖時有三步:
- 如果當前有寫鎖或者讀鎖。如果只有讀鎖,返回false,因為這時如果可以寫,那么讀線程得到的數據就有可能錯誤;如果有寫鎖,但是線程不同,即不符合寫鎖重入規(guī)則,返回false
- 如果寫鎖的數量將會超過最大值65535,拋出異常;否則,寫鎖重入
- 如果沒有讀鎖或寫鎖的話,如果需要阻塞或者CAS失敗,返回false;否則將當前線程置為獲得寫鎖的線程
從上面可以看到調用了writerShouldBlock方法,FairSync的實現是如果等待隊列中有等待線程,則返回false,說明公平模式下,只要隊列中有線程在等待,那么后來的這個線程也是需要記入隊列等待的;NonfairSync中的直接返回的直接是false,說明不需要阻塞。從上面的代碼可以得出,當沒有鎖時,如果使用的非公平模式下的寫鎖的話,那么返回false,直接通過CAS就可以獲得寫鎖。
總結
從上面分析可以得出結論:
- 如果當前沒有寫鎖或讀鎖時,第一個獲取鎖的線程都會成功,無論該鎖是寫鎖還是讀鎖。
- 如果當前已經有了讀鎖,那么這時獲取寫鎖將失敗,獲取讀鎖有可能成功也有可能失敗
- 如果當前已經有了寫鎖,那么這時獲取讀鎖或寫鎖,如果線程相同(可重入),那么成功;否則失敗
釋放鎖
獲取鎖要做的是更改AQS的狀態(tài)值以及將需要等待的線程放入到隊列中;釋放鎖要做的就是更改AQS的狀態(tài)值以及喚醒隊列中的等待線程來繼續(xù)獲取鎖。
讀鎖的釋放
ReadLock的unlock方法如下:
public void unlock() {
sync.releaseShared(1);
}
調用了Sync的releaseShared方法,該方法在AQS中提供,如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
調用tryReleaseShared方法嘗試釋放鎖,如果釋放成功,調用doReleaseShared嘗試喚醒下一個節(jié)點。
AQS的子類需要實現tryReleaseShared方法,Sync中的實現如下:
protected final boolean tryReleaseShared(int unused) {
//得到調用unlock的線程
Thread current = Thread.currentThread();
//如果是第一個獲得讀鎖的線程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
}
//否則,是HoldCounter中計數-1
else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//死循環(huán)
for (;;) {
int c = getState();
//釋放一把讀鎖
int nextc = c - SHARED_UNIT;
//如果CAS更新狀態(tài)成功,返回讀鎖是否等于0;失敗的話,則重試
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
從上面可以看到,釋放鎖的第一步是更新firstReader或HoldCounter的計數,接下來進入死循環(huán),嘗試更新AQS的狀態(tài),一旦更新成功,則返回;否則,則重試。
釋放讀鎖對讀線程沒有影響,但是可能會使等待的寫線程解除掛起開始運行。所以,一旦沒有鎖了,就返回true,否則false;返回true后,那么則需要釋放等待隊列中的線程,這時讀線程和寫線程都有可能再獲得鎖。
寫鎖的釋放
WriteLock的unlock方法如下:
public void unlock() {
sync.release(1);
}
Sync的release方法使用的AQS中的,如下:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
調用tryRelease嘗試釋放鎖,一旦釋放成功了,那么如果等待隊列中有線程再等待,那么調用unparkSuccessor將下一個線程解除掛起。
Sync需要實現tryRelease方法,如下:
protected final boolean tryRelease(int releases) {
//如果沒有線程持有寫鎖,但是仍要釋放,拋出異常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
//如果沒有寫鎖了,那么將AQS的線程置為null
if (free)
setExclusiveOwnerThread(null);
//更新狀態(tài)
setState(nextc);
return free;
}
從上面可以看到,寫鎖的釋放主要有三步:
- 如果當前沒有線程持有寫鎖,但是還要釋放寫鎖,拋出異常
- 得到解除一把寫鎖后的狀態(tài),如果沒有寫鎖了,那么將AQS的線程置為null
- 不管第二步中是否需要將AQS的線程置為null,AQS的狀態(tài)總是要更新的
從上面可以看到,返回true當且只當沒有寫鎖的情況下,還有寫鎖則返回false。
總結
從上面的分析可以得出:
- 如果當前是寫鎖被占有了,只有當寫鎖的數據降為0時才認為釋放成功;否則失敗。因為只要有寫鎖,那么除了占有寫鎖的那個線程,其他線程即不可以獲得讀鎖,也不能獲得寫鎖
- 如果當前是讀鎖被占有了,那么只有在寫鎖的個數為0時才認為釋放成功。因為一旦有寫鎖,別的任何線程都不應該再獲得讀鎖了,除了獲得寫鎖的那個線程。
其他方法
看完了ReentrantReadWriteLock中的讀鎖的獲取和釋放,寫鎖的獲取和釋放,再來看一下其余的一些輔助方法來加深我們對讀寫鎖的理解。
getOwner()
getOwner方法用于返回當前獲得寫鎖的線程,如果沒有線程占有寫鎖,那么返回null。實現如下:
protected Thread getOwner() {
return sync.getOwner();
}
可以看到直接調用了Sync的getOwner方法,下面是Sync的getOwner方法:
final Thread getOwner() {
// Must read state before owner to ensure memory consistency
return ((exclusiveCount(getState()) == 0) ?
null :
getExclusiveOwnerThread());
}
如果獨占鎖的個數為0,說明沒有線程占有寫鎖,那么返回null;否則返回占有寫鎖的線程。
getReadLockCount()
getReadLockCount()方法用于返回讀鎖的個數,實現如下:
public int getReadLockCount() {
return sync.getReadLockCount();
}
Sync的實現如下:
final int getReadLockCount() {
return sharedCount(getState());
}
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
從上面代碼可以看出,要想得到讀鎖的個數,就是看AQS的state的高16位。這和前面講過的一樣,高16位表示讀鎖的個數,低16位表示寫鎖的個數。
getReadHoldCount()
getReadHoldCount()方法用于返回當前線程所持有的讀鎖的個數,如果當前線程沒有持有讀鎖,則返回0。直接看Sync的實現即可:
final int getReadHoldCount() {
//如果沒有讀鎖,自然每個線程都是返回0
if (getReadLockCount() == 0)
return 0;
//得到當前線程
Thread current = Thread.currentThread();
//如果當前線程是第一個讀線程,返回firstReaderHoldCount參數
if (firstReader == current)
return firstReaderHoldCount;
//如果當前線程不是第一個讀線程,得到HoldCounter,返回其中的count
HoldCounter rh = cachedHoldCounter;
//如果緩存的HoldCounter不為null并且是當前線程的HoldCounter,直接返回count
if (rh != null && rh.tid == getThreadId(current))
return rh.count;
//如果緩存的HoldCounter不是當前線程的HoldCounter,那么從ThreadLocal中得到本線程的HoldCounter,返回計數
int count = readHolds.get().count;
//如果本線程持有的讀鎖為0,從ThreadLocal中移除
if (count == 0) readHolds.remove();
return count;
}
從上面的代碼中,可以看到兩個熟悉的變量,firstReader和HoldCounter類型。這兩個變量在讀鎖的獲取中接觸過,前面沒有細說,這里細說一下。HoldCounter類的實現如下:
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
readHolds是ThreadLocalHoldCounter類,定義如下:
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
可以看到,readHolds存儲了每一個線程的HoldCounter,而HoldCounter中的count變量就是用來記錄線程獲得的寫鎖的個數。所以可以得出結論:Sync維持總的讀鎖的個數,在state的高16位;由于讀線程可以同時存在,所以每個線程還保存了獲得的讀鎖的個數,這個是通過HoldCounter來保存的。
除此之外,對于第一個讀線程有特殊的處理,Sync中有如下兩個變量:
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
firstReader表示第一個得到讀鎖的線程,firstReaderHoldCount表示這個線程獲得的寫鎖。所以可以得出結論:第一個獲取到讀鎖的信息保存在firstReader中;其余獲取到讀鎖的線程的信息保存在HoldCounter中。
看完了HoldCounter和firstReader,再來看一下getReadLockCount的實現,主要有三步:
- 當前沒有讀鎖,那么自然每一個線程獲得的讀鎖都是0;
- 如果當前線程是第一個獲取到讀鎖的線程,那么返回firstReadHoldCount;
- 如果當前線程不是第一個獲取到讀鎖的線程,得到該線程的HoldCounter,然后返回其count字段。如果count字段為0,說明該線程沒有占有讀鎖,那么從readHolds中移除。獲取HoldCounter分為兩步,第一步是與cachedHoldCounter比較,如果不是,則從readHolds中獲取。
getWriteLockCount()
getWriteLockCount()方法返回寫鎖的個數,Sync的實現如下:
final int getWriteHoldCount() {
return isHeldExclusively() ? exclusiveCount(getState()) : 0;
}
可以看到如果沒有線程持有寫鎖,那么返回0;否則返回AQS的state的低16位。
總結
當分析ReentranctReadWriteLock時,或者說分析內部使用AQS實現的工具類時,需要明白的就是AQS的state代表的是什么。ReentrantLockReadWriteLock中的state同時表示寫鎖和讀鎖的個數。為了實現這種功能,state的高16位表示讀鎖的個數,低16位表示寫鎖的個數。AQS有兩種模式:共享模式和獨占模式,讀寫鎖的實現中,讀鎖使用共享模式;寫鎖使用獨占模式;另外一點需要記住的即使,當有讀鎖時,寫鎖就不能獲得;而當有寫鎖時,除了獲得寫鎖的這個線程可以獲得讀鎖外,其他線程不能獲得讀鎖。
關注我的技術公眾號,不定期會有優(yōu)質技術文章推送。微信掃一掃下方二維碼即可關注:
微信公眾號二維碼