系列文章目錄和關於我
閱讀此文需要有AQS獨佔和AQS共享的原始碼功底,推薦閱讀:
1.JUC原始碼學習筆記1——AQS獨佔模式和ReentrantLock
2.JUC原始碼學習筆記2——AQS共享和Semaphore,CountDownLatch
一丶類結構和原始碼註釋解讀
1.ReadWriteLock
維護了一對關聯鎖,一個用於只讀操作,另一個用於寫入。讀讀可以共享資源,因為不會造成資料的變更,讀寫,寫寫互斥,因為讀寫可能造成髒讀,幻讀,不可重複讀等錯誤,寫寫可能造成髒寫等錯誤(ps:有點mysql innodb隔離級別的味道,只是mysql innodb 使用mvcc多版本併發控制讓併發能力更高,當然innodb也有S鎖,和X鎖,類似於讀寫鎖一樣對併發事務進行控制)。讀寫鎖在訪問共享資料時允許比互斥鎖更高階別的併發性。與使用互斥鎖相比,讀寫鎖是否會提高效能取決於資料被讀取的頻率,讀寫鎖適用於讀多寫少的情況,如果寫操作變得頻繁,那麼資料的大部分時間都被獨佔鎖定,併發性幾乎沒有增加。
讀寫鎖面臨的問題:
- 寫鎖被釋放時,存在多個執行緒拿讀鎖,和多個執行緒拿寫鎖,是將鎖給讀執行緒還是寫執行緒呢,通常情況下都是給寫鎖的,因為我們認為寫操作沒用讀操作頻繁。如果讀操作非常頻繁,且持有讀鎖的時間很長,寫鎖需要等待所有讀鎖釋放才能獲取,這樣將造成寫鎖長時間無法獲取鎖。當然這種情況下可以使用”公平“的策略,先來後到獲取鎖
- 鎖是否可重入,讀鎖是否可重入,寫鎖是否可重入
- 讀鎖是否可以升級為寫鎖,寫鎖是否可以降級為讀鎖
2.ReentrantReadWriteLock
ReadWriteLock的一個實現,它具備以下特性。
2.1支援公平和非公平
-
非公平
非公平情況下,會導致沒拿到鎖的執行緒處於”飢餓“狀態,但是擁有更高的吞吐率。為什麼?我認為是公平情況下需要排隊,排隊的執行緒會被
LockSupport.park
掛起,意味著釋放鎖的時候需要使用LockSupport.unpark
喚醒排隊的執行緒,這時候並不允許其他執行緒搶佔先機,喚醒是需要時間的,公平情況下這一段時間鎖是沒用被任何執行緒獲取鎖的,所以說吞吐率不如非公平鎖。 -
公平
當構造為公平時,執行緒已近似到達順序策略來競爭進入(CAS入隊的順序,為什麼說是近似順序,入隊的瞬間存在消耗完時間片,讓老六執行緒搶先一步,這個順序是cpu決定的,並不是絕對時間上的先後順序)。當前持有的鎖被釋放時,等待時間最長的單個寫入執行緒將被分配寫入鎖,或者如果有一組讀取執行緒等待時間超過所有等待的寫入執行緒,則該組將被分配讀取鎖(這裡的意思是說,如果寫鎖排在佇列頭部,那麼寫鎖被持有,如果佇列頭部是一堆讀鎖,那麼讀鎖被持有)。
公平情況下,如果寫鎖被持有,或者存在等待寫鎖的執行緒,那麼在此之後獲取讀鎖的執行緒會被阻塞。在最早等待獲取寫鎖的執行緒沒有釋放鎖的情況下,其他執行緒是無法獲取讀鎖的,除非等待獲取寫鎖的執行緒,放棄獲取寫鎖並在AQS佇列中不存在其他等待寫鎖的執行緒位於讀鎖之前。
注意,非阻塞ReentrantReadWriteLock.ReadLock.tryLock()和ReentrandReadWriterLock.WriteLock.tryLock()方法不支援這種公平設定,如果可能的話,不管等待的執行緒如何,都會立即獲取鎖
2.2 可重入,寫鎖降級為讀鎖,讀鎖無法升級為寫鎖
ReentrantReadWriteLock允許讀執行緒和寫執行緒以ReentrantLock類似的方式重新獲取讀或寫鎖。``此外,獲取寫鎖的執行緒可以獲取讀讀鎖,僅持有讀鎖的執行緒試圖獲取寫鎖,它將死鎖
。註釋裡面還提了一嘴這個重入,以及寫鎖可以拿到讀鎖有啥用——A方法獲取寫鎖,呼叫B方法,B方法是讀取資料進行校驗,這時候B需要獲取讀鎖,B呼叫C方法,C也需要獲取讀鎖,這些方法可以正常進行,可以看出重入和寫鎖可降級的用處了吧
可以先持有寫鎖,然後獲取讀鎖(這時候肯定直接可以拿到)然後釋放掉寫鎖,將寫鎖降級為讀鎖,這種使用方式可以保證,持有寫鎖的執行緒一定可以成功拿到讀鎖。
2.3讀寫鎖都支援等待獲取鎖的途中響應中斷
這是synchronized
所不支援的,在ReentrantLock
原始碼解讀中,解讀過原始碼,重點是LockSupport.park
方法掛起執行緒A,執行緒A有兩種方式可以從LockSupport.park
中返回:1.被unpark,2.被中斷。原始碼的實現是如果不支援等待鎖的途中中斷,會記錄下當前執行緒被中斷過,然後Thread.interrupted()
重置中斷標註(因為中斷的執行緒無法再次被park)然後繼續park當前執行緒,讓其等待鎖,獲取到鎖之後,發現之前被中斷過會自我中斷補上中斷標誌。在獲取鎖的途中響應中斷,則是從LockSupport.park
檢查中斷標誌,如果被中斷了說明是由於中斷從LockSupport.park
中返回,這時候將丟擲中斷異常。
2.4 寫鎖支援基於Condition的等待喚醒
寫鎖支援Condition,但是讀鎖不支援Condition等待喚醒,讀鎖本身是共享的,需要所有讀鎖釋放後才有必要喚醒寫鎖。
二丶讀寫鎖使用範例
這裡使用讀寫鎖實現了一個執行緒安全,讀寫分離的TreeMap,doug lea還提醒到想讓這個TreeMap併發效能很好,必須實在讀多寫少的情況下。
三丶屬性和構造方法
可以看到讀寫鎖,內部使用final修飾讀鎖和寫鎖,然後透過writeLock,readLock
兩個方法將鎖暴露出去。有意思的是其構造方法,構造讀寫鎖把this傳遞了進去
可以看到傳遞this的目的,是讓讀寫鎖使用sync = fair ? new FairSync() : new NonfairSync();
生成的AQS子類物件,讓讀寫鎖使用同一個Sync物件,這樣才能做到讀寫互斥,下面我們分析FairSync
,NonfairSync
是怎麼一回事
三丶原始碼解析
1.FairSync,NonfairSync類結構
熟悉的套路,把具體鎖的實現,放到內部類Sync中,讀寫鎖只是呼叫對應的Sync的方法,整個Sync內容內容很多,我們先看具體原始碼,Sync自然就柳暗花明了。
2.讀鎖加鎖-ReadLock#lock
2.1原始碼解析
問題:
讀鎖如何實現公平,
讀鎖如何實現重入(重入需要記錄獲取次數,那麼doug lea 如何實現的),
寫鎖被獲取如何讓其他執行緒無法獲取讀鎖,寫鎖如何降級為讀鎖
獲取讀鎖然後獲取寫鎖為啥會死鎖
ReadLock的lock
方法直接呼叫Sync
的acquireShared(1)
方法,此方法在AQS中進行了實現
最終是呼叫Sync的tryAcquireShared
方法
protected final int tryAcquireShared(int unused) {
//當前執行緒
Thread current = Thread.currentThread();
//狀態 低16為寫鎖重入次數 高16位讀鎖被多少個執行緒持有
int c = getState();
//寫鎖被佔有 且不是自己佔有寫鎖 返回-1 表示獲取失敗,這個時候會共享入隊
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//寫鎖共享次數
int r = sharedCount(c);
//readerShouldBlock 公平情況下就是看前面是否有人排隊
//非公平情況下就是看頭結點的下一個節點是否是共享模式,如果是共享說明,寫鎖被持有多個讀鎖都被阻塞了
if (!readerShouldBlock() &&
//讀鎖被持有小於最大值
r < MAX_COUNT &&
//CAS更改成功讀鎖數量
compareAndSetState(c, c + SHARED_UNIT)) {
//當前讀鎖是第一個拿讀鎖的
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//讀鎖執行緒再次獲取讀鎖
firstReaderHoldCount++;
} else {
//記錄當前執行緒持有讀鎖數量
//利用ThreadLocal進行記錄
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;
}
//到這 1.當前執行緒需要排隊,2.讀鎖被持有數量大於最大值 3.CAS失敗,多個執行緒在拿讀鎖
return fullTryAcquireShared(current);
}
這段程式碼可以看做兩部分,最後一句return之前的內容,我稱為快速獲取寫鎖
,fullTryAcquireShared
我稱之為完全嘗試獲取寫鎖
-
快速獲取寫鎖
首先如果發現獨佔鎖重入數量不為0,且獨佔的執行緒不是自己,也就是說當前寫鎖被其他執行緒持有,那麼直接返回-1,這裡可以看出
寫鎖被持有,其他執行緒無法獲取讀鎖
其次
readerShouldBlock
這個方法返回true 代表當前讀執行緒需要阻塞,什麼是否需要阻塞?- 公平鎖FairSync的邏輯是看是否有排隊的執行緒,前面有人排隊為什麼說讀執行緒需要阻塞——說明寫鎖被持有,多個讀執行緒在排隊,這時候是看是否有人排隊,這是公平的體現
- 非公平鎖是看第一個排隊的執行緒是否是獨佔模式,這是不公平的體現,如果第一個排隊的執行緒是共享模式,那麼還是會嘗試獲取鎖,相當於插隊
douglea,使用CAS修改state記錄寫鎖被多少執行緒持有,這裡CAS是因為可能存在多個執行緒獲取讀鎖,修改成功後,會用ThreadLocal記錄當前這個執行緒寫鎖重入數量
-
完全獲取寫鎖
進入完全獲取寫鎖的條件
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
判斷寫鎖沒有被持有,但是readerShouldBlock
發現讀執行緒需要排隊,可能是突然寫鎖被持有,也可能是讀鎖被很多執行緒持有,當前執行緒需要排隊- cas更新state失敗,意味著多個執行緒獲取寫鎖,當前執行緒cas失敗,有點類似之前AQS中的快速入隊
完全獲取寫鎖的邏輯和快速類似,但是它是自選+CAS保證,要麼獲取到寫鎖,要麼返回-1進行排隊
2.2讀鎖如何實現公平
上面說到了,是readerShouldBlock
來實現公平,在公平鎖的情況下,加入當前寫鎖被持有,多個讀執行緒在排隊,如 A->B->C,然後執行緒D嘗試獲取讀鎖,這時候是看佇列中是否有排隊的執行緒,如果有那麼執行緒D會進行排隊。
非公平鎖是看第一個排隊的執行緒是否是獨佔模式,如果佇列頭是獨佔,那麼會進行排隊,這樣做的目的是避免寫執行緒一直飢餓,如果不是那麼會嘗試獲取鎖,這相當於廁所門口多人(讀執行緒)排隊,你硬搶,是非公平的體現
2.3讀鎖如何實現重入(重入需要記錄獲取次數,那麼doug lea 如何實現的)
對於第一個獲取寫鎖的執行緒,它會使用firstReader
,firstReaderHoldCount
記錄執行緒和其重入數量,對於後面獲取寫鎖的執行緒,會使用ThreadLocal
進行記錄
2.4寫鎖被獲取如何讓其他執行緒無法獲取讀鎖,寫鎖如何降級為讀鎖
首先快速獲取讀鎖的有如下判斷
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
如果持有寫鎖的執行緒不是當前執行緒,那麼返回-1,進行排隊,在完全入隊的自旋中也有這一段邏輯
如果當前執行緒就是持有寫鎖的執行緒則不會返回-1,依舊還是自旋+CAS獲取讀鎖,從而獲取到讀鎖
然後此時執行緒再釋放寫鎖,就實現了寫鎖降級為讀鎖。
2.5獲取讀鎖然後獲取寫鎖為啥會死鎖
執行上述程式碼,你會發現發生了死鎖,make it
永遠不會列印出來,為啥呢?這裡需要我們看完寫鎖獲取的原始碼。
3.讀鎖釋放ReadLock#unLock
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//更新當前執行緒的重入數量
if (firstReader == current) {
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} 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;
}
//cas改變state
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
原始碼不難,主要兩部分
-
更改ThreadLocal,或者是
firstReaderHoldCount
中記錄的當前執行緒寫鎖重入數量如果是第一個獲取寫鎖的執行緒那麼
firstReaderHoldCount--
完全釋放的時候firstReader
設定為null如果不是第一個或者說第一個執行緒重入2次,釋放3此,都會進入到else分支,減少ThreadLocal中記錄的重入數量,如果發現釋放次數>重入次數,會丟擲異常
-
cas改變state,state使用低16位記錄寫鎖重入數量,使用高16為記錄讀鎖重入數量
低16位 = 寫鎖重入數
高16位 = 每一個讀鎖持有執行緒的重入數之和
這裡需要使用CAS修改state,因為存在多個讀執行緒同時釋放讀鎖的情況
讀鎖的tryLock lockInterruptibly()還是哪些老套路,沒啥好看的
4.寫鎖加鎖-WriteLock#Lock
問題
寫鎖如何實現公平,
寫鎖如何實現重入(重入需要記錄獲取次數,那麼doug lea 如何實現的),
獲取讀鎖然後獲取寫鎖為啥會死鎖
4.1.原始碼解析
寫鎖加鎖呼叫的是AQS的acquire
方法
我們在AQS獨佔原始碼分析中,說到過,如果tryAcquire
失敗,意味著後續會呼叫acquireQueued
入隊然後獲取鎖後才能出隊,其中selfInterrupt
是為了將獲取鎖途中受到的中斷補上,tryAcquire
被讀寫鎖中的Sync內部類重寫,如下
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
//寫鎖被重入數
int w = exclusiveCount(c);
//c!=0說明寫鎖或者讀鎖至少有一個鎖被持有
if (c != 0) {
//w == 0說明讀鎖被持有,那麼寫鎖需要阻塞返回false
//w!=0 但是 current != getExclusiveOwnerThread() 說明寫鎖被其他執行緒持有
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//到這說明寫鎖被當前執行緒持有,那麼執行緒不需要使用CAS
//確保重入數不能大於 MAX_COUNT
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//重入+1
setState(c + acquires);
return true;
}
//writerShouldBlock——寫鎖是否需要阻塞
//公平鎖的情況下:如果有排隊的執行緒 那麼返回true
//非公平的情況下,恆定返回false
if (writerShouldBlock() ||
//如果cas失敗 說明寫鎖被其他執行緒持有 那麼返回false需要進行排隊 寫寫互斥
!compareAndSetState(c, c + acquires))
return false;
//到此說明當前執行緒 拿到了寫鎖 記錄下獨佔的執行緒
setExclusiveOwnerThread(current);
return true;
}
原始碼相比於共享更簡單,主要分為兩部分
-
寫鎖或者讀鎖被持有
這部分的重點在於處理重入
C!=0
說明state不為0,那麼寫或者讀鎖必定有一種鎖被持有,然後(w == 0 || current != getExclusiveOwnerThread()
,如果w==0成立,說明讀鎖被持有了,這時候直接返回false,因為讀寫互斥,如果w!=0說明此時寫鎖被持有,繼續判斷current != getExclusiveOwnerThread()
,當前執行緒是否是持有寫鎖的執行緒,如果不是返回false,說明寫鎖被其他執行緒持有。如果當前執行緒就是持有寫鎖的執行緒,接下來就是使用
setState
設定重入數量,這一步不需要CAS,本身便是執行緒安全的 -
寫鎖和讀鎖都未被持有
這裡的未被持有,是上面第一個if中的判斷結果,也許第一個if執行完就有其他執行緒獲取到了讀鎖或者寫鎖
首先
writerShouldBlock
判斷寫執行緒是否需要阻塞,在公平情況下是看是否具備排隊的執行緒,非公平情況下恆定返回false,只有第一個if結束有其他執行緒搶先獲取了寫鎖,並且後續有其他執行緒獲取鎖,才可能出現公平情況下判斷得到有排隊的執行緒,這是一種公平的體現,其他執行緒在排隊那麼當前獲取鎖的執行緒也需要加入佇列尾部。非公平情況下,writerShouldBlock
恆定返回false,這裡可以看出寫鎖偏好
——即使前面有A已經獲取了寫鎖,BC兩個執行緒排隊獲取讀鎖,非公平情況下,只要A釋放了寫鎖的一瞬間,當前執行緒可不管釋放有人排毒,就是一個CAS搶鎖,這裡即是非公平
也是寫鎖偏好
的體現compareAndSetState(c, c + acquires)
這便是當前執行緒CAS搶鎖,保證執行緒安全,後續如果搶鎖成功setExclusiveOwnerThread(current)
記錄寫鎖被當前執行緒獲取。
4.2寫鎖如何實現公平
上面說到,公平情況下,寫鎖的獲取需要看是否具備排隊的執行緒,如果具備其他執行緒排隊,那麼直接返回false,這樣會讓當前執行緒呼叫acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
進行排隊並等待其他執行緒釋放鎖後等待,這便是公平。
4.3寫鎖如何實現重入(重入需要記錄獲取次數,那麼doug lea 如何實現的)
和ReentrantLock一樣,透過state記錄重入數量,只是低16位才是寫鎖的重入數量,每當寫鎖被重入便setState
進行記錄
4.4獲取讀鎖然後獲取寫鎖為啥會死鎖
前面讀鎖加鎖解讀,我們就丟擲了這個問題,這裡我們結合寫鎖加鎖原始碼看下,為什麼會死鎖
5.寫鎖釋放鎖-WriteLock#unLock
寫鎖的釋放就是呼叫內部類Sync的release方法,此方法會呼叫tryRelease
如果返回true後續會喚醒其他等待的執行緒
ReentrantReadWriteLock內部類Sync的tryRelease方法如下
這裡首先會判斷當前執行緒釋放是獨佔寫鎖的執行緒,如果不是那麼就是其他執行緒想釋放持有寫鎖執行緒的鎖,這是不允許的
然後計算釋放後的重入數量,這裡一般是重入數量-1,如果減少重入後的數量為0,那麼free為true,這意味著完全釋放了寫鎖,會將獨佔寫鎖的執行緒屬性置為null,如果free為true這個方法結束後就會呼叫AQS中的unparkSuccessor
喚醒其他執行緒
setState
改變重入數量,這裡不需要加鎖因為還沒有喚醒其他執行緒,所以此時不會存線上程安全問題,這是獨佔釋放和共享釋放的一個區別,共享鎖的釋放需要使用自旋+CAS保證執行緒安全的更新state