讀寫鎖在同一時刻可以允許多個讀線程訪問,但是在寫線程訪問時针炉,所有的讀
線程和其他寫線程均被阻塞督勺。讀寫鎖維護了一對鎖渠羞,一個讀鎖和一個寫鎖,通過分離讀鎖和寫鎖智哀,使得并發(fā)性相比一般的排他鎖有了很大提升次询。
一般情況下,讀寫鎖的性能都會比排它鎖好瓷叫,因為大多數(shù)場景讀是多于寫的屯吊。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量摹菠。Java并發(fā)包提供讀寫鎖的實現(xiàn)是ReentrantReadWriteLock盒卸,它提供的特性如表所示:
讀寫鎖的實現(xiàn)分析
1 讀寫狀態(tài)的設(shè)計
讀寫鎖同樣依賴自定義同步器來實現(xiàn)同步功能,而讀寫狀態(tài)就是其同步器的同步狀態(tài)次氨。讀寫鎖的自定義同步器需要在同步狀態(tài)(一個整型變量)上維護多個讀線程和一個寫線程的狀態(tài)蔽介,使得該狀態(tài)的設(shè)計成為讀寫鎖實現(xiàn)的關(guān)鍵。
如果在一個整型變量上維護多種狀態(tài)煮寡,就一定需要“按位切割使用”這個變量虹蓄,讀寫鎖將變量切分成了兩個部分,高16位表示讀幸撕,低16位表示寫薇组,劃分方式如圖所示:
讀寫鎖是如何迅速確定讀和寫各自的狀態(tài)呢?答案是通過位運算杈帐。假設(shè)當(dāng)前同步狀態(tài)值為S体箕,寫狀態(tài)等于S&0x0000FFFF(將高16位全部抹去),讀狀態(tài)等于S>>>16(無符號補0右移16位)挑童。當(dāng)寫狀態(tài)增加1時累铅,等于S+1,當(dāng)讀狀態(tài)增加1時站叼,等于S+(1<<16)娃兽,也就是S+0x00010000。
根據(jù)狀態(tài)的劃分能得出一個推論:S不等于0時尽楔,當(dāng)寫狀態(tài)(S&0x0000FFFF)等于0時投储,則讀狀態(tài)(S>>>16)大于0第练,即讀鎖已被獲取。
2 寫鎖的獲取與釋放
寫鎖是一個支持重進入的排它鎖玛荞,獲取情況有兩種
- 如果當(dāng)前線程已經(jīng)獲取了寫鎖娇掏,則增加寫狀態(tài)。
- 如果當(dāng)前線程在獲取寫鎖時勋眯,讀鎖已經(jīng)被獲扔の唷(讀狀態(tài)不為0)或者該線程不是已經(jīng)獲取寫鎖的線程,則當(dāng)前線程進入等待狀態(tài)客蹋,代碼如下所示:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 存在讀鎖或者當(dāng)前獲取線程不是已經(jīng)獲取寫鎖的線程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 增加寫鎖狀態(tài)
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
return false;
}
setExclusiveOwnerThread(current);
return true;
}
3 讀鎖的獲取與釋放
讀鎖是一個支持重進入的共享鎖塞蹭,它能夠被多個線程同時獲取。
- 在沒有其他寫線程訪問(或者寫狀態(tài)為0)時讶坯,讀鎖總會被成功地獲取番电,而所做的也只是(線程安全的)增加讀狀態(tài)
- 如果當(dāng)前線程已經(jīng)獲取了讀鎖,則增加讀狀態(tài)辆琅。
- 如果當(dāng)前線程在獲取讀鎖時漱办,寫鎖已被其他線程獲取,則進入等待狀態(tài)涎跨。
protected final int tryAcquireShared(int unused) {
for (;;) {
int c = getState();
int nextc = c + (1 << 16);
if (nextc < c)
throw new Error("Maximum lock count exceeded");
// 如果當(dāng)前線程不是獲取寫鎖的線程洼冻,則獲取讀鎖失敗
if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
return -1;
// 如果當(dāng)前線程是獲取寫鎖的線程(鎖降級)
// 或?qū)戞i未被線程獲取,則獲取讀鎖
if (compareAndSetState(c, nextc))
return 1;
}
}
4 鎖降級
如果當(dāng)前線程擁有寫鎖隅很,然后將其釋放撞牢,最后再獲取讀鎖,這種分段完成的過程不能稱之為鎖降級叔营。
鎖降級是指把持孜荼搿(當(dāng)前擁有的)寫鎖,再獲取到讀鎖绒尊,隨后釋放(先前擁有的)寫鎖的過程畜挥。
那么鎖降級的設(shè)計的目的是什么呢?為何要在擁有寫鎖的前提下去獲取讀鎖婴谱?
通過查看一些文章蟹但,寫一下自己的理解:
鎖降級的目的其實是為了讓線程對數(shù)據(jù)變化敏感,如果先釋放寫鎖谭羔,再獲取讀鎖华糖,可能在獲取之前,會有其他線程獲取到寫鎖瘟裸,阻塞讀鎖的獲取客叉,就無法感知數(shù)據(jù)變化了。所以需要先hold住寫鎖,保證數(shù)據(jù)無變化兼搏,獲取讀鎖卵慰,然后再釋放寫鎖。
例如有多個線程對同一塊數(shù)據(jù)區(qū)域data進行讀寫操作佛呻,要求對每次數(shù)據(jù)的更改敏感裳朋。假設(shè)t1時刻data區(qū)域被寫線程將狀態(tài)s0更改為s1,更改完后若直接釋放鎖吓著,那么可能會有其他線程獲取寫鎖再扭,將data區(qū)域的狀態(tài)從s1更改為s2,這樣一來整個過程就無法感知到data區(qū)域的s1狀態(tài)夜矗。
如果采用了鎖降級,那么獲取寫鎖的線程t將data區(qū)域狀態(tài)更改為s1后便持有讀鎖让虐,那么其它想獲取寫鎖的線程將會阻塞紊撕,直到線程t將讀鎖釋放,那么這個過程中將會感知到data區(qū)域的s1狀態(tài)赡突。