日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java锁详解之改进读写锁StampedLock

發布時間:2025/3/20 java 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java锁详解之改进读写锁StampedLock 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

      • 先了解一下ReentrantReadWriteLock
      • StampedLock重要方法
      • StampedLock示例
      • StampedLock可能出現的性能問題
      • StampedLock原理
      • StampedLock源碼分析

先了解一下ReentrantReadWriteLock

當系統存在讀和寫兩種操作的時候,讀和讀之間并不會對程序結果產生影響。所以后來設計了ReentrantReadWriteLock這種讀寫分離鎖,它做到了讀與讀之間不用等待
示例:

// 讀寫鎖private static ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();// 讀鎖private static ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();// 寫鎖private static ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();// 共享資源private static Integer count = 0;// 讀操作public Integer readCount() throws InterruptedException {readLock.lock();try{Thread.sleep(1000);return count;}finally {readLock.unlock();}}// 寫操作public void addOne() throws InterruptedException {writeLock.lock();try{Thread.sleep(1000);++count;}finally {writeLock.unlock();}}

這種讀寫分離鎖的缺點是,只有讀讀操作不會競爭鎖,即讀與讀操作是并行的,而讀寫、寫寫都會競爭鎖。很明顯讀寫其實大部分情況也都可以不競爭鎖的,這就是后來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);}

從上面代碼也能看出,在讀方法里面,必須是以下順序寫代碼:

  • long stamp = s1.tryOptimisticRead();
  • 讀取共享資源
  • 判斷在第2步時,共享資源是否被更改:s1.validate(stamp)
  • StampedLock鎖適用于讀多寫少的場景

    StampedLock可能出現的性能問題

    StampedLock內部實現時,使用類似于CAS操作的死循環反復嘗試的策略。
    在它掛起線程時,使用的是Unsafe.park()函數,而park()函數在遇到線程中斷時,會直接返回(不會拋出異常)。而在StampedLock的死循環邏輯中,沒有處理有關中斷的邏輯。因此,這就會導致阻塞在park()上的線程被中斷后,會再次進入循環。而當退出條件得不到滿足時,就會發生瘋狂占用CPU的情況。
    下面演示了這個問題:

    public class StampedLockCUPDemo {static Thread[] holdCpuThreads = new Thread[3];static final StampedLock lock = new StampedLock();public static void main(String[] args) throws InterruptedException {new Thread() {public void run(){long readLong = lock.writeLock();// 一直占著鎖,可使其他線程被掛起LockSupport.parkNanos(6100000000L);lock.unlockWrite(readLong);}}.start();Thread.sleep(100);for( int i = 0; i < 3; ++i) {holdCpuThreads [i] = new Thread(new HoldCPUReadThread());holdCpuThreads [i].start();}Thread.sleep(10000);// 中斷三個線程:中斷是問題的關鍵原因for(int i=0; i<3; i++) {holdCpuThreads [i].interrupt();}}private static class HoldCPUReadThread implements Runnable { public void run() {// 獲取讀鎖,將被阻塞,循環long lockr = lock.readLock();System.out.println(Thread.currentThread().getName() + " get read lock");lock.unlockRead(lockr);}} }

    上面獲取讀鎖被park()阻塞的線程由于中斷操作,導致線程再次進入死循環,導致線程一直處于RUNNABLE狀態,消耗著CPU資源。當拿到鎖后,這種情況就會消失。這種情況在實際上出現的概率是較低的。

    StampedLock原理

    針對ReentrantReadWriteLock只能讀與讀并行,而讀與寫不能并行的問題,JDK8實現了StampedLock。
    StampedLock的內部實現是基于CLH鎖的。CLH鎖是一種自旋鎖,它保證沒有饑餓發生,并且可以保證FIFO的服務順序。
    CLH鎖的基本思想如下:鎖維護一個等待線程隊列,所有申請鎖,但是沒有成功的線程都記錄在這個隊列中。每一個節點(一個節點代表一個線程),保存一個標記位(locked),用于判斷當前線程是否已經釋放鎖。
    當一個線程試圖獲得鎖時,取得當前等待隊列的尾部節點作為其前序節點,并使用類似如下代碼判斷前序節點是否已經成功釋放:

    while(pred.locked) { }

    只要前序節點(pred)沒有釋放鎖,則表示當前線程還不能繼續執行,因此會自旋等待。
    反之,如果前序線程已經釋放鎖,則當前線程可以繼續執行。
    釋放鎖時,也遵循這個邏輯,線程會將自身節點的locked位置標記為false,那么后續等待的線程就能繼續執行了。
    在StampedLock內部,為維護一個等待鏈表隊列:

    static final class WNode {volatile WNode prev;volatile WNode next;volatile WNode cowait; // list of linked readersvolatile Thread thread; // non-null while possibly parkedvolatile int status; // 0, WAITING, or CANCELLEDfinal int mode; // RMODE or WMODEWNode(int m, WNode p) { // 構造器mode = m; prev = p;}}/** Head of CLH queue */private transient volatile WNode whead;/** Tail (last) of CLH queue */private transient volatile WNode wtail;

    上述代碼中,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,如果不一致,則宣告樂觀鎖失敗。
    樂觀鎖失敗后,則可以提升鎖級別,使用悲觀讀鎖。

    public long readLock() {long s, next; // bypass acquireRead on fully unlocked case onlyreturn ((((s = state) & ABITS) == 0L &&U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ?next : acquireRead(false, 0L)); }

    悲觀讀會嘗試設置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的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。