介紹
除了重入鎖ReentrantLock以外它碎,Doug Lea大神還順帶實現(xiàn)了讀寫重入鎖ReentrantReadWriteLock坠宴,依舊支持重入特性、公平與非公平模式,分出了讀鎖和寫鎖叼丑。
讀鎖
當讀鎖被持有后,會阻止其他線程獲取寫鎖掠河,但讀鎖不排斥其他線程持有讀鎖栈虚。
寫鎖
當寫鎖被持有后,既排斥其他線程申請讀鎖汁掠,還排斥其他線程申請寫鎖略吨。
結(jié)構(gòu)
具體的結(jié)構(gòu)關系,直接上圖:
流程
通過ReentrantReadWriteLock的readLock()獲取讀鎖考阱,writeLock()獲取寫鎖翠忠,讀鎖和寫鎖都有l(wèi)ock()、lockInterruptibly()乞榨、tryLock()秽之、tryLock(long timeout, TimeUnit unit)当娱,unlock()。
lock()
方法考榨,遵循初始化時的公平鎖或非公平鎖模式請求鎖跨细,請求鎖的過程不會被中斷,直到持有鎖為止河质。
lockInterruptibly()
方法冀惭,如果線程調(diào)用了interrupted(),請求鎖的過程將會中斷愤诱,并且拋出InterruptedException異常云头。如果線程沒有被中斷,那么會堅持到持有鎖為止淫半。
tryLock()
方法溃槐,無視初始化時公平與非公平模式,直接嘗試一次請求鎖的CAS操作科吭,如果成功昏滴,返回true,失敗对人,返回false谣殊。
tryLock(long timeout, TimeUnit unit)
方法,類似lockInterruptibly()牺弄,但是比lockInterruptibly()多了超時檢查操作姻几,當超時時,會中斷請求鎖過程势告。
unlock()
方法蛇捌,釋放鎖對象當前持有的鎖一次。
申請讀鎖
因為請求寫鎖鎖的流程與普通重入鎖大同小異咱台,所以這里只展示讀鎖的lock()方法的請求流程络拌,lock()
方法中通過sync調(diào)用acquireShared()
方法,acquireShared()
方法的實現(xiàn)在AbstractQueuedSynchronizer中:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
所有讀鎖會進行一次tryAcquireShared(int arg)
和doAcquireShared(int arg)
回溺。獲取寫鎖則調(diào)用acquire(int arg)
方法春贸,方法中if條件中調(diào)用tryAcquire(arg)
和 acquireQueued(final Node node, int arg)
,與讀鎖獲取的流程有些小差別遗遵。
回到正題萍恕,tryAcquireShared(int arg)
方法被Sync實現(xiàn),再看ReentrantReadWriteLock.Sync中:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} 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;
}
return fullTryAcquireShared(current);
}
這個方法中有兩個比較重要的流程车要,都會在特定條件下返回雄坪,先看看進入這兩個流程的if條件。
第一個if條件,exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
维哈,先檢查寫鎖重入數(shù)绳姨,如果不為0,再檢查當前線程是否已經(jīng)持有寫鎖阔挠,如果未持有寫鎖則返回-1表示失敗飘庄,那么可以看出,同一個線程是可以同時持有寫鎖和讀鎖的购撼。
第二個if條件跪削,!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)
首先如果是公平鎖,那么在readerShouldBlock()中會進入阻塞隊列等待迂求,如果非公平鎖碾盐,就調(diào)用apparentlyFirstQueuedIsExclusive()
方法檢查隊列第一個節(jié)點是否在等待寫鎖,如果不是揩局,再來檢查讀鎖持有是否超過最大值毫玖,如果未超過,再來CAS修改讀鎖持有數(shù)量凌盯,CAS操作成功之后付枫,這個if流程中會修改部分引用和緩存信息,這個具體作用后面再說驰怎。
在公平鎖模式阻塞重新被喚醒后或者已有其他線程持有讀鎖阐滩,又或者讀鎖當前持有數(shù)現(xiàn)在已經(jīng)到了MAX_COUNT時,都會跳過第二個if流程直接進入fullTryAcquireShared(Thread current)
方法中:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
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.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
//省略部分代碼
}
return 1;
}
}
}
fullTryAcquireShared(Thread current)
方法中的請求鎖的過程其實和tryAcquireShared(int unused)
中的過程十分相似症杏,最重要的還是CAS操作的這個方法衅疙。只有compareAndSetState()
成功之后才會結(jié)束fullTryAcquireShared(Thread current)
方法中for循環(huán),結(jié)束這次讀鎖請求操作鸳慈。
exclusiveCount(int c)與sharedCount(int c)
在fullTryAcquireShared(Thread current)
方法與tryAcquireShared(int unused)
方法中中,變量c喧伞,就是持有鎖的個數(shù)走芋,通過getState()獲取到值后,先exclusiveCount(int c)潘鲫,再經(jīng)過sharedCount(int c)后賦值給r翁逞,找到和它們有關的代碼:
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);// 65536
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;// 65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;// 65535
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
exclusiveCount(int c)操作會清除整型變量c高16位的所有數(shù)值,保留低16位數(shù)值溉仑。
sharedCount(int c) 操作清除整型變量c低16位挖函,保留高16位數(shù)值。
所以r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)
就是在驗證寫鎖持有數(shù)量浊竟,當c = 0
時c + SHARED_UNIT = 65536
怨喘,經(jīng)過sharedCount(int c)
計算后得1津畸,c + SHARED_UNIT
相當于對 c = (c >>> 16 + 1) << 16,對c高16位數(shù)值加1必怜。
也就是說state值的高位保存著讀鎖數(shù)量肉拓,低位保存寫鎖數(shù)量,讀鎖與讀鎖最大持有數(shù)都是65535梳庆。
關于讀寫鎖等待隊列
不管是公平鎖還是非公平鎖暖途,未能在其他線程釋放鎖時的特殊時刻正好獲取到鎖,都會進入等待鎖隊列膏执,而這個隊列被讀鎖與寫鎖共用驻售,具體邏輯很多與重入鎖高度重合,有興趣可以看我寫的這篇博客ReentrantLock源碼通讀(一)更米。
關于HoldCounter
HoldCounter遍布在所有讀鎖的請求和釋放過程當中欺栗,并且為了緩存這個讀鎖持有數(shù),代碼占據(jù)了這些流程代碼量大概兩成左右壳快,所以HoldCounter的重要性不言而喻纸巷,那么具體是什么作用呢?
在ReentrantReadWriteLock中對外提供了一個方法:
/**
* Queries the number of reentrant read holds on this lock by the
* current thread. A reader thread has a hold on a lock for
* each lock action that is not matched by an unlock action.
*
* @return the number of holds on the read lock by the current thread,
* or zero if the read lock is not held by the current thread
* @since 1.6
*/
public int getReadHoldCount() {
return sync.getReadHoldCount();
}
//Sync中getReadHoldCount()
final int getReadHoldCount() {
if (getReadLockCount() == 0)
return 0;
Thread current = Thread.currentThread();
if (firstReader == current)
return firstReaderHoldCount;
HoldCounter rh = cachedHoldCounter;
if (rh != null && rh.tid == getThreadId(current))
return rh.count;
int count = readHolds.get().count;
if (count == 0) readHolds.remove();
return count;
}
注釋翻譯過來就是眶痰,查詢當前線程重入讀鎖的次數(shù)瘤旨。不是可以通過Sync中的state全局變量獲取重入次數(shù)嗎,讀鎖與寫鎖是通過int的高低位來區(qū)分的竖伯,十分方便存哲,為什么不能用這個state來獲取讀鎖的重入次數(shù)呢?
事實上寫鎖的確是利用state的來獲取寫鎖的重入次數(shù)的七婴,但是寫鎖是一個線程獨占的祟偷,它會排斥其他線程獲取寫鎖以及讀鎖。
讀鎖打厘,因為其可被共享的特殊性質(zhì)修肠,導致state中高位存儲的是多個線程重入后的總數(shù),所以state無法用來查詢當前線程重入讀鎖的次數(shù)户盯,當前線程無法知曉自己重入次數(shù)嵌施,這在某些復雜場景或者使用不規(guī)范的情況下,可能造成讀鎖無法被正常釋放莽鸭,而且排查起來也異常困難吗伤。
所以HoldCounter出現(xiàn)了,與HoldCounter配合當然少不了ThreadLocal硫眨,ReentrantReadWriteLock.Sync中包含有HoldCounter靜態(tài)內(nèi)部類和ThreadLocalHoldCounter
靜態(tài)內(nèi)部類足淆。
HoldCounter
記錄了重入計數(shù)與線程的tid。
ThreadLocalHoldCounter繼承ThreadLocal,重載了initialValue()
方法巧号。
并且為了更快的獲取重入數(shù)族奢,還額外贈送了兩個全局變量,cachedHoldCounter和firstReaderHoldCount裂逐。
cachedHoldCounter
保存上個請求讀鎖線程的HoldCounter歹鱼。
firstReaderHoldCount
保存第一個獲取讀鎖線程的重入數(shù)。
所以Sync實現(xiàn)的getReadHoldCount()
方法根據(jù)return卜高,分為四個分支弥姻,通過各種全局緩存變量,盡量高效的返回結(jié)果掺涛。
第一個分支庭敦,讀取state的高位,如果為0返回薪缆。
第二個分支秧廉,檢查當前線程是否等于firstReader
,如果是拣帽,返回firstReaderHoldCount
疼电。
第三個分支,檢查cachedHoldCounter中的tid是否等于當前線程的tid减拭,如果是蔽豺,返回cachedHoldCounter.count
。
第四個分支拧粪,就是遍歷ThreadLocal中的threadMap找到當前線程的HoldCounter返回count
修陡。
思考
一個線程是否可以同時獲取到寫鎖與讀鎖?
肯定可以可霎,不然也不會在注釋里說明會產(chǎn)生死鎖的場景了魄鸦。
根據(jù)寫鎖的tryAcquire(int arg)
源碼:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
先判斷了state != 0
,再判斷w==0 || current != getExclusiveOwnerThread()
直接堵死線程獲取讀鎖在獲取寫鎖的可能性癣朗。
所以能同時獲取讀鎖和寫鎖的操作只能是先獲取寫鎖拾因,然后獲取讀鎖。
如何能成功同時獲取讀鎖和寫鎖呢旷余?
在非公平模式下绢记,線程持有寫鎖后,去請求讀鎖荣暮,而此時等待線程隊列中第一個節(jié)點不是請求寫鎖的線程,即可成功獲取讀鎖與寫鎖罩驻,但如果是請求寫鎖的線程穗酥,會返回失敗狀態(tài)值-1,之后進入等待線程隊列,線程在持有寫鎖的情況下休眠并且等待通知被喚醒砾跃,由于喚醒超時線程的觸發(fā)點實在有線程釋放鎖之后骏啰,所以這時無論是否設置過超時參數(shù),都是無效的抽高,依然會導致死鎖判耕,所以在使用時需要格外注意。
總結(jié)
讀鎖可以與其他線程共享翘骂,但排斥其他線程獲取寫鎖壁熄。
寫鎖排斥其他線程獲取鎖,當前持有寫鎖后碳竟,仍然可以去獲取讀鎖草丧,完全獨占,但是寫鎖競爭激烈時莹桅,完全獨占的操作也很容易導致死鎖昌执。
讀寫鎖的持有或重入上限均是65535。
當前線程重入計數(shù)诈泼,讀寫鎖是分開保存的懂拾,state中保存的是讀鎖重入的總數(shù),寫鎖不受影響铐达。
當前線程讀鎖重入次數(shù)是使用ThreadLocal保存的岖赋,每個線程單獨維護一個HoldCounter。使用的三個全局變量緩存特殊狀態(tài)的線程重入計數(shù)變量娶桦,盡量快的返回結(jié)果贾节。