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