Java锁详解之改进读写锁StampedLock
文章目錄
- 先了解一下ReentrantReadWriteLock
- StampedLock重要方法
- StampedLock示例
- StampedLock可能出現(xiàn)的性能問題
- StampedLock原理
- StampedLock源碼分析
先了解一下ReentrantReadWriteLock
當(dāng)系統(tǒng)存在讀和寫兩種操作的時候,讀和讀之間并不會對程序結(jié)果產(chǎn)生影響。所以后來設(shè)計了ReentrantReadWriteLock這種讀寫分離鎖,它做到了讀與讀之間不用等待。
示例:
這種讀寫分離鎖的缺點(diǎn)是,只有讀讀操作不會競爭鎖,即讀與讀操作是并行的,而讀寫、寫寫都會競爭鎖。很明顯讀寫其實(shí)大部分情況也都可以不競爭鎖的,這就是后來StampedLock的優(yōu)化點(diǎn)。
StampedLock重要方法
| long readLock() | 獲取讀鎖(悲觀鎖),如果資源正在被修改,則當(dāng)前線程被阻塞 。返回值是在釋放鎖時會用到的stamp |
| long tryReadLock() | 獲取讀鎖 ,如果拿到鎖則返回一個在釋放鎖時會用到的stamp。如果拿不到鎖,直接返回0,且不阻塞線程 |
| long readLockInterruptibly() | 獲取讀鎖,可中斷 |
| void unlockRead(long stamp) | 如果參數(shù)里的stamp與該讀鎖的stamp一致,則釋放讀鎖 |
| long writeLock() | 獲取寫鎖(悲觀鎖),如果拿不到鎖則當(dāng)前線程被阻塞。返回值在釋放鎖時會用到 |
| long tryWriteLock() | 獲取寫鎖,如果拿到鎖則返回一個在釋放鎖時會用到的stamp。如果拿不到鎖,則直接返回0,且不阻塞線程 |
| long writeLockInterruptibly() | 獲取寫鎖,可中斷 |
| void unlockWrite(long stamp) | 如果參數(shù)里的stamp與該寫鎖的stamp一致,則釋放寫鎖 |
| long tryOptimisticRead() | 樂觀讀,返回一個后面會被驗(yàn)證的stamp,如果資源被鎖住了,則返回0 |
| boolean validate(long stamp) | stamp值沒有被修改過,返回true,否則返回false。如果stamp為0,始終返回false。 |
StampedLock示例
以下是官方例子:
public class Point {//內(nèi)部定義表示坐標(biāo)點(diǎn)private double x, y;private final StampedLock s1 = new StampedLock();void move(double deltaX, double deltaY) {// 獲取寫鎖,并拿到此時的stamplong stamp = s1.writeLock();try {x += deltaX;y += deltaY;} finally {// 釋放寫鎖時,傳入了獲取寫鎖的stamp,// 這也就是下面讀方法里面validate能判斷stamp是否被修改的原因s1.unlockWrite(stamp);}}//只讀方法double distanceFormOrigin() {// 試圖嘗試一次樂觀讀 返回一個類似于時間戳的整數(shù)stamp,它在后面的validate方法中將被驗(yàn)證。// 如果資源已經(jīng)被鎖住了,則返回0long stamp = s1.tryOptimisticRead(); //讀取x和y的值。這時候我們并不確定x和y是否是一致的double currentX = x, currentY = y;// 判斷這個stamp在讀的過程中是否被修改過// 如果stamp沒有被修改過返回true,被修改過返回false。如果stamp為0,是返回false。if (!s1.validate(stamp)) { // 發(fā)現(xiàn)被修改了,這里使用readLock()獲得悲觀的讀鎖,并進(jìn)一步讀取數(shù)據(jù)。// 如果當(dāng)前對象正在被修改,則讀鎖的申請可能導(dǎo)致線程掛起。stamp = s1.readLock();try {currentX = x;currentY = y;} finally {s1.unlockRead(stamp);//退出臨界區(qū),釋放讀鎖}}return Math.sqrt(currentX * currentX + currentY * currentY);} }上面讀方法中,如果發(fā)現(xiàn)資源被修改了,還可以通過像JDK7中AtomicInteger類里的CAS操作那樣寫一個死循環(huán)(JDK8不是這樣寫的),通過不斷的嘗試使得最終拿到鎖:
double distanceFormOrigin() {double currentX , currentY ;for(;;){long stamp = s1.tryOptimisticRead();currentX = x;currentY = y;if(s1.validate(stamp)){break; }} return Math.sqrt(currentX * currentX + currentY * currentY);}從上面代碼也能看出,在讀方法里面,必須是以下順序?qū)懘a:
StampedLock鎖適用于讀多寫少的場景
StampedLock可能出現(xiàn)的性能問題
StampedLock內(nèi)部實(shí)現(xiàn)時,使用類似于CAS操作的死循環(huán)反復(fù)嘗試的策略。
在它掛起線程時,使用的是Unsafe.park()函數(shù),而park()函數(shù)在遇到線程中斷時,會直接返回(不會拋出異常)。而在StampedLock的死循環(huán)邏輯中,沒有處理有關(guān)中斷的邏輯。因此,這就會導(dǎo)致阻塞在park()上的線程被中斷后,會再次進(jìn)入循環(huán)。而當(dāng)退出條件得不到滿足時,就會發(fā)生瘋狂占用CPU的情況。
下面演示了這個問題:
上面獲取讀鎖被park()阻塞的線程由于中斷操作,導(dǎo)致線程再次進(jìn)入死循環(huán),導(dǎo)致線程一直處于RUNNABLE狀態(tài),消耗著CPU資源。當(dāng)拿到鎖后,這種情況就會消失。這種情況在實(shí)際上出現(xiàn)的概率是較低的。
StampedLock原理
針對ReentrantReadWriteLock只能讀與讀并行,而讀與寫不能并行的問題,JDK8實(shí)現(xiàn)了StampedLock。
StampedLock的內(nèi)部實(shí)現(xiàn)是基于CLH鎖的。CLH鎖是一種自旋鎖,它保證沒有饑餓發(fā)生,并且可以保證FIFO的服務(wù)順序。
CLH鎖的基本思想如下:鎖維護(hù)一個等待線程隊(duì)列,所有申請鎖,但是沒有成功的線程都記錄在這個隊(duì)列中。每一個節(jié)點(diǎn)(一個節(jié)點(diǎn)代表一個線程),保存一個標(biāo)記位(locked),用于判斷當(dāng)前線程是否已經(jīng)釋放鎖。
當(dāng)一個線程試圖獲得鎖時,取得當(dāng)前等待隊(duì)列的尾部節(jié)點(diǎn)作為其前序節(jié)點(diǎn),并使用類似如下代碼判斷前序節(jié)點(diǎn)是否已經(jīng)成功釋放:
只要前序節(jié)點(diǎn)(pred)沒有釋放鎖,則表示當(dāng)前線程還不能繼續(xù)執(zhí)行,因此會自旋等待。
反之,如果前序線程已經(jīng)釋放鎖,則當(dāng)前線程可以繼續(xù)執(zhí)行。
釋放鎖時,也遵循這個邏輯,線程會將自身節(jié)點(diǎn)的locked位置標(biāo)記為false,那么后續(xù)等待的線程就能繼續(xù)執(zhí)行了。
在StampedLock內(nèi)部,為維護(hù)一個等待鏈表隊(duì)列:
上述代碼中,WNode(wait node)為鏈表的基本元素,每一個WNode表示一個等待線程。字段whead和wtail分別指向等待鏈表的頭部和尾部。
另外一個重要的字段為state:
private transient volatile long state;字段state表示當(dāng)前鎖的狀態(tài)。它是一個long型,有64位,其中,倒數(shù)第8位表示寫鎖狀態(tài),如果該位為1,表示當(dāng)前由寫鎖占用。
對于一次樂觀讀的操作,它會執(zhí)行如下操作:
public long tryOptimisticRead() {long s;return (((s = state) & WBIT) == 0L) ? (s & SBITS) : 0L; }一次成功的樂觀讀必須保證當(dāng)前鎖沒有寫鎖占用。其中WBIT用來獲取寫鎖狀態(tài)位,值為0x80。如果成功,則返回當(dāng)前state的值(末尾7位清零,末尾7位表示當(dāng)前正在讀取的線程數(shù)量)。
如果在樂觀讀后,有線程申請了寫鎖,那么state的狀態(tài)就會改變:
public long writeLock() {long s, next; // bypass acquireWrite in fully unlocked case onlyreturn ((((s = state) & ABITS) == 0L &&U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ?next : acquireWrite(false, 0L));}上述代碼中第4行,設(shè)置寫鎖位為1(通過加上WBIT(0x80))。這樣,就會改變state的取值。那么在樂觀鎖確認(rèn)(validate)時,就會發(fā)現(xiàn)這個改動,而導(dǎo)致樂觀鎖失敗。
public boolean validate(long stamp) {// See above about current use of getLongVolatile herereturn (stamp & SBITS) == (U.getLongVolatile(this, STATE) & SBITS); }上述validate()函數(shù)比較當(dāng)前stamp和發(fā)生樂觀鎖時取得的stamp,如果不一致,則宣告樂觀鎖失敗。
樂觀鎖失敗后,則可以提升鎖級別,使用悲觀讀鎖。
悲觀讀會嘗試設(shè)置state狀態(tài),它會將state加1,用于統(tǒng)計讀線程的數(shù)量。如果失敗,則進(jìn)入acquireRead()二次嘗試鎖獲取。
在acquireRead()中,線程會在不同條件下進(jìn)行若干次自旋,試圖通過CAS操作獲得鎖。如果自旋宣告失敗,則會啟用CLH隊(duì)列,將自己加到隊(duì)列中。之后再進(jìn)行自旋,如果發(fā)現(xiàn)自己成功獲得了讀鎖,則會進(jìn)一步把自己cowait隊(duì)列中的讀線程全部激活(使用Usafe.unpark()方法)。如果最終依然無法成功獲得讀鎖,則會使用Unsafe.park()方法掛起當(dāng)前線程。
方法acquireWrite()和acquireRead()也非常類似,也是通過自旋嘗試、加入等待隊(duì)列、直至最終Unsafe.park()掛起線程的邏輯進(jìn)行的。釋放鎖時與加鎖動作相反,以unlockWrite()為例:
public void unlockWrite(long stamp) {WNode h;if (state != stamp || (stamp & WBIT) == 0L)throw new IllegalMonitorStateException();state = (stamp += WBIT) == 0L ? ORIGIN : stamp;//將寫標(biāo)記位清零if ((h = whead) != null && h.status != 0)release(h); }上述代碼,將寫標(biāo)記位清零,如果state發(fā)生溢出,則退回到初始值。
接著,如果等待隊(duì)列不為空,則從等待隊(duì)列中激活一個線程(絕大部分情況下是第1個等待線程)繼續(xù)執(zhí)行release(h)。
StampedLock源碼分析
https://blog.csdn.net/ryo1060732496/article/details/88973923
https://blog.csdn.net/huzhiqiangCSDN/article/details/76694836
總結(jié)
以上是生活随笔為你收集整理的Java锁详解之改进读写锁StampedLock的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java锁详解之ReentrantLoc
- 下一篇: Java消息中间件(activeMQ)