JDK8併發包新增StampedLock鎖

4.8 JDK8新增的StampedLock鎖探究

StampedLock是併發包裏面jdk8版本新增的一個鎖,該鎖提供了三種模式的讀寫控制,三種模式分別以下:數據庫

  • 寫鎖writeLock,是個排它鎖或者叫獨佔鎖,同時只有一個線程能夠獲取該鎖,當一個線程獲取該鎖後,其它請求的線程必須等待,當目前沒有線程持有讀鎖或者寫鎖的時候才能夠獲取到該鎖,請求該鎖成功後會返回一個stamp票據變量用來表示該鎖的版本,當釋放該鎖時候須要unlockWrite並傳遞參數stamp。
  • 悲觀讀鎖readLock,是個共享鎖,在沒有線程獲取獨佔寫鎖的狀況下,同時多個線程能夠獲取該鎖,若是已經有線程持有寫鎖,其餘線程請求獲取該讀鎖會被阻塞。這裏講的悲觀實際上是參考數據庫中的樂觀悲觀鎖的,這裏說的悲觀是說在具體操做數據前悲觀的認爲其餘線程可能要對本身操做的數據進行修改,因此須要先對數據加鎖,這是在讀少寫多的狀況下的一種考慮,請求該鎖成功後會返回一個stamp票據變量用來表示該鎖的版本,當釋放該鎖時候須要unlockRead並傳遞參數stamp。
  • 樂觀讀鎖tryOptimisticRead,是相對於悲觀鎖來講的,在操做數據前並無經過CAS設置鎖的狀態,若是當前沒有線程持有寫鎖,則簡單的返回一個非0的stamp版本信息,獲取該stamp後在具體操做數據前還須要調用validate驗證下該stamp是否已經不可用,也就是看當調用tryOptimisticRead返回stamp後到到當前時間間是否有其餘線程持有了寫鎖,若是是那麼validate會返回0,否者就可使用該stamp版本的鎖對數據進行操做。因爲tryOptimisticRead並無使用CAS設置鎖狀態因此不須要顯示的釋放該鎖。該鎖的一個特色是適用於讀多寫少的場景,由於獲取讀鎖只是使用與或操做進行檢驗,不涉及CAS操做,因此效率會高不少,可是同時因爲沒有使用真正的鎖,在保證數據一致性上須要拷貝一份要操做的變量到方法棧,而且在操做數據時候可能其餘寫線程已經修改了數據,而咱們操做的是方法棧裏面的數據,也就是一個快照,因此最多返回的不是最新的數據,可是一致性仍是獲得保障的。

下面經過JDK8註釋裏面的一個例子講解來加深對上面講解的理解。編程

class Point {

    // 成員變量
    private double x, y;

    // 鎖實例
    private final StampedLock sl = new StampedLock();

    // 排它鎖-寫鎖(writeLock)
    void move(double deltaX, double deltaY) {
        long stamp = sl.writeLock();
        try {
            x += deltaX;
            y += deltaY;
        } finally {
            sl.unlockWrite(stamp);
        }
    }

    // 樂觀讀鎖(tryOptimisticRead)
    double distanceFromOrigin() {

        // 嘗試獲取樂觀讀鎖(1)
        long stamp = sl.tryOptimisticRead();
        // 將所有變量拷貝到方法體棧內(2)
        double currentX = x, currentY = y;
        // 檢查在(1)獲取到讀鎖票據後,鎖有沒被其餘寫線程排它性搶佔(3)
        if (!sl.validate(stamp)) {
            // 若是被搶佔則獲取一個共享讀鎖(悲觀獲取)(4)
            stamp = sl.readLock();
            try {
                // 將所有變量拷貝到方法體棧內(5)
                currentX = x;
                currentY = y;
            } finally {
                // 釋放共享讀鎖(6)
                sl.unlockRead(stamp);
            }
        }
        // 返回計算結果(7)
        return Math.sqrt(currentX * currentX + currentY * currentY);
    }

    // 使用悲觀鎖獲取讀鎖,並嘗試轉換爲寫鎖
    void moveIfAtOrigin(double newX, double newY) {
        // 這裏可使用樂觀讀鎖替換(1)
        long stamp = sl.readLock();
        try {
            // 若是當前點在原點則移動(2)
            while (x == 0.0 && y == 0.0) {
                // 嘗試將獲取的讀鎖升級爲寫鎖(3)
                long ws = sl.tryConvertToWriteLock(stamp);
                // 升級成功,則更新票據,並設置座標值,而後退出循環(4)
                if (ws != 0L) {
                    stamp = ws;
                    x = newX;
                    y = newY;
                    break;
                } else {
                    // 讀鎖升級寫鎖失敗則釋放讀鎖,顯示獲取獨佔寫鎖,而後循環重試(5)
                    sl.unlockRead(stamp);
                    stamp = sl.writeLock();
                }
            }
        } finally {
            // 釋放鎖(6)
            sl.unlock(stamp);
        }
    }
}

如上代碼Point類裏面有兩個成員變量,和三個操做成員變量的方法,另外實例化了一個StampedLock對象用來保證操做的原子性。多線程

首先分析下move方法,該函數做用是在添加增量,改變當前point座標的位置,代碼先獲取到了寫鎖,而後對point座標進行修改,而後釋放鎖。該鎖是排它鎖,這保證了其餘線程調用move函數時候會被阻塞,直到當前線程顯示釋放了該鎖,也就是保證了對變量x,y操做的原子性。併發

而後看下distanceFromOrigin方法,該方法做用是計算當前位置到原點的距離,代碼(1)首先嚐試獲取樂觀讀鎖,若是當前沒有其它線程獲取到了寫鎖,那麼(1)會返回一個非0的stamp用來表示版本信息,代碼(2)拷貝變量到本地方法棧裏面,代碼(3)檢查在(1)獲取到的票據是否還有效,之因此還要在此校驗是由於代碼(1)獲取讀鎖時候並無經過CAS操做修改鎖的狀態而是簡單的經過與或操做返回了一個版本信息,這裏校驗是看在在獲取版本信息到如今的時間段裏面是否有其餘線程持有了寫鎖,若是有則以前獲取的版本信息就無效了。這裏若是校驗成功則執行(7)使用本地方法棧裏面的值進行計算而後返回。須要注意的是在代碼(3)校驗成功後,代碼(7)計算中其餘線程可能獲取到了寫鎖而且修改了x,y的值,而當前線程執行代碼(7)進行計算時候採用的纔是對修改前值的拷貝,也就是操做的值是對以前值的一個拷貝,並非新的值。另外還有個問題,代碼(2)和(3)可否互換,答案是不能,假設位置換了,那麼首先執行validate,假如驗證經過了,要拷貝x,y值到本地方法棧,而在拷貝的過程當中頗有可能其餘線程已經修改了x,y中的一個,這就形成了數據的不一致性了。那麼你可能會問,那不交換(2)和(3)時候在拷貝x,y值到本地方法棧裏面時候也會存在其餘線程修改了x,y中的一個值那,這個確實會存在,可是,別忘了拷貝後還有一道validate,若是這時候有線程修改了x,y中的值,那麼確定是有線程在調用validate前sl.tryOptimisticRead後獲取了寫鎖,那麼validate時候就會失敗。如今應該明白了吧,這也是樂觀讀設計的精妙之處也是使用時候容易出問題的地方。下面繼續分析validate失敗後會執行代碼(4)獲取悲觀讀鎖,若是這時候騎行線程持有寫鎖則代碼(4)會致使的當前線程阻塞直到其它線程釋放了寫鎖。獲取到讀鎖後,代碼(5)拷貝變量到本地方法棧,而後就是代碼(6)釋放了鎖,拷貝的時候因爲加了讀鎖在拷貝期間其它線程獲取寫鎖時候會被阻塞,這保證了數據的一致性。最後代碼(7)使用方法棧裏面數據計算返回,同理這裏在計算時候使用的數據也可能不是最新的,其它寫線程可能已經修改過原來的x,y值了。函數

最後一個方法moveIfAtOrigin方法做用是若是當前座標爲原點則移動到指定的位置。代碼(1)獲取悲觀讀鎖,保證其它線程不能獲取寫鎖修改x,y值,而後代碼(2)判斷若是當前點在原點則更新座標,代碼(3)嘗試升級讀鎖爲寫鎖,這裏升級不必定成功,由於多個線程均可以同時獲取悲觀讀鎖,當多個線程都執行到(3)時候只有一個能夠升級成功,升級成功則返回非0的stamp,否非返回0,這裏假設當前線程升級成功,而後執行步驟(4)更新stamp值和座標值而後退出循環,若是升級失敗則執行步驟(5)首先釋放讀鎖而後申請寫鎖,獲取到寫鎖後在循環從新設置座標值。最後步驟(6)釋放鎖。性能

使用樂觀讀鎖仍是很容易犯錯誤的,必需要當心,必需要保證以下的使用順序:測試

long stamp = lock.tryOptimisticRead(); //非阻塞獲取版本信息
copyVaraibale2ThreadMemory();//拷貝變量到線程本地堆棧
if(!lock.validate(stamp)){ // 校驗
    long stamp = lock.readLock();//獲取讀鎖
    try {
        copyVaraibale2ThreadMemory();//拷貝變量到線程本地堆棧
     } finally {
       lock.unlock(stamp);//釋放悲觀鎖
    }
    
}

useThreadMemoryVarables();//使用線程本地堆棧裏面的數據進行操做

總結: 相比ReentrantLock讀寫鎖,StampedLock經過提供樂觀讀鎖在多線程多寫線程少的狀況下提供更好的性能,由於樂觀讀鎖不須要進行CAS設置鎖的狀態而只是簡單的測試狀態。更具體測試數據期待Java併發編程基礎之併發包源碼剖析一書的出版。線程

相關文章
相關標籤/搜索