1.簡(jiǎn)介
ReentrantReadWriteLock采用的是“悲觀讀”的策略孵奶,當(dāng)?shù)谝粋€(gè)讀線程拿到鎖之后,第二個(gè)、第三個(gè)讀線程還可以拿到鎖翘魄,使得寫線程一直拿不到鎖,可能導(dǎo)致寫線程“餓死”舀奶。雖然在其公平或非公平的實(shí)現(xiàn)中暑竟,都盡量避免這種情形(寫鎖偏向性,優(yōu)先級(jí)高)育勺,但還有可能發(fā)生光羞。
StampedLock引入了“樂(lè)觀讀”策略绩鸣,讀的時(shí)候不加讀鎖,讀出來(lái)發(fā)現(xiàn)數(shù)據(jù)被修改了纱兑,再升級(jí)為“悲觀讀”呀闻,相當(dāng)于降低了“讀”的地位,把搶鎖的天平往“寫”的一方傾斜了一下潜慎,避免寫線程被餓死捡多。
2.使用場(chǎng)景
class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
// 多個(gè)線程調(diào)用該方法,修改x和y的值
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX; y += deltaY;
} finally {
sl.unlockWrite(stamp);
} }
// 多個(gè)線程調(diào)用該方法铐炫,求距離
double distenceFromOrigin() {
// 使用“樂(lè)觀讀”
long stamp = sl.tryOptimisticRead();
// 將共享變量拷貝到線程棧
double currentX = x, currentY = y;
// 讀期間有其他線程修改數(shù)據(jù)
if (!sl.validate(stamp)) {
// 讀到的是臟數(shù)據(jù)垒手,丟棄。
// 重新使用“悲觀讀”
stamp = sl.readLock();
try {
currentX = x; currentY = y;
} finally { sl.unlockRead(stamp);
} }
return Math.sqrt(currentX * currentX + currentY * currentY);
} }
有一個(gè)Point類倒信,多個(gè)線程調(diào)用move()方法科贬,修改坐標(biāo);還有多個(gè)線程調(diào)用distanceFromOrigin()方法鳖悠,求距離榜掌。
首先,執(zhí)行move操作的時(shí)候乘综,要加寫鎖憎账。這個(gè)用法和ReadWriteLock的用法沒(méi)有區(qū)別,寫操作和寫操作也是互斥的卡辰。
關(guān)鍵在于讀的時(shí)候胞皱,用了一個(gè)“樂(lè)觀讀”sl.tryOptimisticRead(),相當(dāng)于在讀之前給數(shù)據(jù)的狀態(tài)做了一個(gè)“快照”九妈。然后反砌,把數(shù)據(jù)拷貝到內(nèi)存里面,在用之前萌朱,再比對(duì)一次版本號(hào)于颖。如果版本號(hào)變了,則說(shuō)明在讀的期間有其他線程修改了數(shù)據(jù)嚷兔。讀出來(lái)的數(shù)據(jù)廢棄,重新獲取讀鎖做入。
要說(shuō)明的是冒晰,這三行關(guān)鍵代碼對(duì)順序非常敏感,不能有重排序竟块。因?yàn)?state 變量已經(jīng)是volatile壶运,所以可以禁止重排序,但stamp并不是volatile的浪秘。為此蒋情,在validate(stamp)方法里面插入內(nèi)存屏障埠况。
public boolean validate(long stamp) {
VarHandle.acquireFence(); //內(nèi)存屏障
return (stamp & SBITS) == (state & SBITS);
}
3. “樂(lè)觀讀”的實(shí)現(xiàn)原理
首先,StampedLock是一個(gè)讀寫鎖棵癣,因此也會(huì)像讀寫鎖那樣辕翰,把一個(gè)state變量分成兩半,分別表示讀鎖和寫鎖的狀態(tài)狈谊。同時(shí)喜命,它還需要一個(gè)數(shù)據(jù)的version。但是河劝,一次CAS沒(méi)有辦法操作兩個(gè)變量壁榕,所以這個(gè)state變量本身同時(shí)也表示了數(shù)據(jù)的version。
用最低的8位表示讀和寫的狀態(tài)赎瞎,其中第8位表示寫鎖的狀態(tài)牌里,最低的7位表示讀鎖的狀態(tài)。因?yàn)閷戞i只有一個(gè)bit位务甥,所以寫鎖是不可重入的牡辽。
4 悲觀讀/寫:“阻塞”與“自旋”策略實(shí)現(xiàn)差異
StampedLock也要進(jìn)行悲觀的讀鎖和寫鎖操作。不過(guò)缓呛,它不是基于AQS實(shí)現(xiàn)的催享,而是內(nèi)部重新實(shí)現(xiàn)了一個(gè)阻塞隊(duì)列。
- 剛開(kāi)始的時(shí)候哟绊,whead=wtail=NULL因妙,然后初始化,建一個(gè)空節(jié)點(diǎn)票髓,whead和wtail都指向這個(gè)空節(jié)點(diǎn)攀涵,之后往里面加入一個(gè)個(gè)讀線程或?qū)懢€程節(jié)點(diǎn)。
- 基于這個(gè)阻塞隊(duì)列實(shí)現(xiàn)的鎖的調(diào)度策略和AQS很不一樣洽沟,也就是“自旋”以故。
- 不同點(diǎn):在AQS里面,當(dāng)一個(gè)線程CAS state失敗之后裆操,會(huì)立即加入阻塞隊(duì)列怒详,并且進(jìn)入阻塞狀態(tài)。
-
但在StampedLock中踪区,CAS state失敗之后昆烁,會(huì)不斷自旋,自旋足夠多的次數(shù)之后缎岗,如果還拿不到鎖静尼,才進(jìn)入阻塞狀態(tài)。
根據(jù)CPU的核數(shù),定義了自旋次數(shù)的常量值
StampedLock還提供了更復(fù)雜的將悲觀讀鎖升級(jí)為寫鎖的功能鼠渺,它主要使用在if-then-update的場(chǎng)景:即先讀鸭巴,如果讀的數(shù)據(jù)滿足條件,就返回拦盹,如果讀的數(shù)據(jù)不滿足條件鹃祖,再嘗試寫。