上回說到ReentrantLock,今天來談談讀寫鎖(ReentrantLock)和其具體實現ReentrantReadWriteLock。看這篇文章前,強烈建議你回到先讀懂ReentrantLock,因爲ReentrantReadWriteLock其實是在ReentrantLock的基礎上實現的,可以參考我之前的博客ReentrantLock源碼解析
既然有了鎖,爲什麼還需要讀寫鎖?我們來想象下這個場景。你們小區樓下有個公告欄,有時候有人會寫個招租,有時候有人會寫個尋物啓事…… 當然一個人正在改公告欄的時候,另外一個人就不能同時改了,這裏就相當於有了一把無形的鎖,我改的時候就把廣告欄“鎖住”,改完再“解鎖”,當然別人鎖住了之後我也改不了。說完了“寫”再說“讀”,一個人在讀公告欄的時候,別人就不能去寫了,這樣不禮貌,這裏也相當於讀的人用一把“鎖”把公告欄給鎖了。
如果這裏讀者用的鎖和寫者用的鎖是一樣的,那麼這把鎖不緊不然別人寫了,也不讓別人讀了,相當於一個人在看公告欄,別人就不能看了,這明顯不合理啊。 所以要把讀和寫用的鎖區分開來,所有讀的人共享一把鎖,寫的人獨享鎖。放到公告欄的例子上,改公告的時候同時只有一個人可以看,但讀的時候所有人可以同時讀,這樣就可以把“公告欄”這個資源的利用率最大化。
看到這裏,你應該已經理解了什麼叫做“讀寫鎖”,接下來我們直接看下jdk中ReentrantReadWriteLock的實現,再次建議先閱讀ReentrantLock的具體實現。
從類結構圖看,貌似它比ReentrantLock更復雜寫,多兩個內部類 ReadLock 和 WriteLock,看着Lock提供的api完全一樣,看來得從具體實現上來看其二者有什麼樣的差異了。
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
從ReentrantReadWriteLock的構造方法可以看出,它也支持公平鎖和非公平鎖,當然默認也是非公平鎖。和ReentrantLock一樣,加鎖和解鎖的實現邏輯都是在 Sync 裏,所以我們重點看下Sync的實現,代碼太多這裏就不貼完整代碼了,建議讀者自行打開代碼。
Sync
從Sync的類結構圖來看,它還是相當複雜的,別急讓我們來捋一捋,我們先從WriteLock看起(看起來會比較熟悉),看下他的lock和release的具體實現。
@ReservedStackAccess
final boolean tryWriteLock() {
Thread current = Thread.currentThread(); // 1
int c = getState(); // 2
if (c != 0) { // 3
int w = exclusiveCount(c); // 4
if (w == 0 || current != getExclusiveOwnerThread()) // 5
return false;
if (w == MAX_COUNT) //6. MAX_COUNT = 65535
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1)) // 7
return false;
setExclusiveOwnerThread(current); // 8
return true;
}
如果你看過ReentrantLock的話,相信這段代碼你已經完全能看懂了。這裏我再大概說下這段代碼的流程
- 獲取到當前線程。
- 獲取到鎖對象的state值,state是保存了鎖的狀態。
- 如果state不爲0,說明已經有線程加過鎖了,這時候需要額外判斷下,跳到4。 如果state爲0,直接跳到 7。
- 獲取到當前加寫鎖的次數,這裏獲取的是state的低16位。
- c已經不爲0了,如果w不爲0說明有線程加了寫鎖,如果加了寫鎖的線程也不是當前線程的,加鎖就失敗了。
- 這裏需要額外判斷下鎖重入的次數,如果已經到65535就不能再加鎖了,後續會解釋爲什麼是65535。
- 執行CAS操作更改鎖狀態 state。
- 到這裏說明加寫鎖已經成功了,把當前鎖的持有者記錄下來。
@ReservedStackAccess
final boolean tryReadLock() {
Thread current = Thread.currentThread(); // 1
for (;;) {
int c = getState(); // 2
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current) // 3
return false;
int r = sharedCount(c); // 4
if (r == MAX_COUNT) // 5
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // 6
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return true;
}
}
}
讀鎖的加鎖代碼就完全不一樣了,第一眼看到的不同就是這裏有個大大的無限循環,我們還是來看下讀鎖的加鎖過程。
- 獲取當前線程。
- 獲取鎖的state狀態值。
- 如果寫鎖的加鎖次數不是0切寫鎖持有者不是當前線程,加讀鎖失敗。
- 獲取讀鎖的加鎖次數,sharedCount©獲取的是state的高16位。
- 如果讀鎖加鎖次數達到65535,拋Error,和寫鎖一樣,只能加65535次。
- 執行到這,說明可以加鎖,使用CAS更新state成功後這裏就開始記錄一些讀鎖的狀態信息,注意這裏state增加值不是1,而是SHARED_UNIT(65536)。
看完readLock和writeLock的加鎖方式就可以大體理解ReentrantReadWriteLock的實現了,原來它只是把ReentrantLock中的state分成兩部分來用,高16位記錄讀鎖狀態,低16位記錄寫鎖狀態,如下圖。
這也是爲什麼上文中加鎖最大次數是65535的原因了,這也是而是SHARED_UNIT的值爲65536的原因。
理解了加鎖的代碼,解鎖部分也就好理解了,本質上是把加鎖的代碼反向執行下,代碼如下。
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
@ReservedStackAccess
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
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;
}
}
Sync中還有一個ThreadLocalHoldCounter類,這個類的作用其實是記錄每個線程對讀鎖的加鎖測試,見名知意線程級的統計,代碼也很簡單,這裏就不再貼了。
Sync中除了上文說到的幾個加解鎖的API,其餘一些API就是獲取Sync對象中各個狀態的API,沒什麼好說的。
FairSync & NonfairSync
說完了抽象類Sync,我們來說下它的兩個具體實現 FairSync 和 NonfairSync。 這兩個實現類非常非常簡單,只是重寫了 writerShouldBlock() 和 readerShouldBlock() 方法而已,如果你已經知道什麼是公平和非公平了,這地方也就很好理解了。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
// 寫鎖可以始終不被等待隊列裏的線程阻塞,只要當前鎖是未鎖定狀態就可以加鎖
return false;
}
final boolean readerShouldBlock() {
//這個方法判斷隊列的head.next是否正在等待寫鎖,這個方法確保讀鎖不應該讓寫鎖始終等待,即便是非公平的,但寫鎖有更高的優先級,獲取讀鎖還是得排隊。
return apparentlyFirstQueuedIsExclusive();
}
}
// 公平鎖就很好理解了,只要等待隊列不爲空,就得去排隊
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
ReadLock & WriteLock
其實看完Sync裏的邏輯,基本上ReadLock和WriteLock的實現邏輯我們已經知道了。ReadLock和WriteLock只是向用戶提供裏有些功能抽象(實現了Lock中的方法),封裝好了具體的實現,其實具體邏輯還是在Sync中實現。
從類繼承關係來看,二者也只是簡單
結論
瞭解完ReentrantReadWriteLock的實現後你就會發現,它其實和ReentrantLock一樣,之前把ReentrantLock中的state切分成兩部分用,高16位作爲讀鎖的state,低16位作爲寫鎖。如果把ReadLock和WriteLock拉出來單獨看的話,二者都是一個ReentrantLock,只是不能像ReentrantLock那樣重入那麼多次而已。
ReentrantReadWriteLock的出現大幅提升了多讀少寫場景下的性能問題,但它依舊有自己的缺點,就是它可能會導致寫飢餓。還是拿小區公告欄的例子,如果任意時刻都有人在看公告欄,你也不好打斷人家所以你公告更新不了啊,所以想更新的人就得一直等着。
關注我,下次和大家一起看下 StampedLock 是如何解決飢餓問題的。