【高并發】AQS詳解
582
2025-04-09
上回說到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); }
1
2
3
4
5
6
從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; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
如果你看過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; } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
讀鎖的加鎖代碼就完全不一樣了,第一眼看到的不同就是這里有個大大的無限循環,我們還是來看下讀鎖的加鎖過程。
獲取當前線程。
獲取鎖的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; } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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(); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ReadLock & WriteLock
其實看完Sync里的邏輯,基本上ReadLock和WriteLock的實現邏輯我們已經知道了。ReadLock和WriteLock只是向用戶提供里有些功能抽象(實現了Lock中的方法),封裝好了具體的實現,其實具體邏輯還是在Sync中實現。
從類繼承關系來看,二者也只是簡單
結論
了解完ReentrantReadWriteLock的實現后你就會發現,它其實和ReentrantLock一樣,之前把ReentrantLock中的state切分成兩部分用,高16位作為讀鎖的state,低16位作為寫鎖。如果把ReadLock和WriteLock拉出來單獨看的話,二者都是一個ReentrantLock,只是不能像ReentrantLock那樣重入那么多次而已。
ReentrantReadWriteLock的出現大幅提升了多讀少寫場景下的性能問題,但它依舊有自己的缺點,就是它可能會導致寫饑餓。還是拿小區公告欄的例子,如果任意時刻都有人在看公告欄,你也不好打斷人家所以你公告更新不了啊,所以想更新的人就得一直等著。
關注我,下次和大家一起看下 StampedLock 是如何解決饑餓問題的。
任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。