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