天天看點

Java線程安全StampedLock

文章目錄

  • ​​ReadWriteLock​​
  • ​​小結​​

ReadWriteLock

前面介紹的​

​ReadWriteLock​

​可以解決多線程同時讀,但隻有一個線程能寫的問題。

如果我們深入分析​

​ReadWriteLock​

​,會發現它有個潛在的問題:如果有線程正在讀,寫線程需要等待讀線程釋放鎖後才能擷取寫鎖,即讀的過程中不允許寫,這是一種​

​悲觀的讀鎖​

​。

要進一步提升并發執行效率,Java 8引入了新的讀寫鎖:​

​StampedLock​

​StampedLock​

​和​

​ReadWriteLock​

​相比,改進之處在于:讀的過程中也允許擷取寫鎖後寫入!這樣一來,我們讀的資料就可能不一緻,是以,需要一點額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種​

​樂觀鎖​

​樂觀鎖​

​的意思就是樂觀地估計讀的過程中大機率不會有寫入,是以被稱為樂觀鎖。反過來,​

​悲觀鎖​

​則是讀的過程中拒絕有寫入,也就是寫入必須等待。顯然樂觀鎖的并發效率更高,但一旦有小機率的寫入導緻讀取的資料不一緻,需要能檢測出來,再讀一遍就行。

我們來看例子:

public class Point {
private final StampedLock stampedLock = new StampedLock();

private double x;
private double y;

public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 擷取寫鎖
try {
x += deltaX;
y += deltaY;
        } finally {
stampedLock.unlockWrite(stamp); // 釋放寫鎖
        }
    }

public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 獲得一個樂觀讀鎖
// 注意下面兩行代碼不是原子操作
// 假設x,y = (100,200)
double currentX = x;
// 此處已讀取到x=100,但x,y可能被寫線程修改為(300,400)
double currentY = y;
// 此處已讀取到y,如果沒有寫入,讀取是正确的(100,200)
// 如果有寫入,讀取是錯誤的(100,400)
if (!stampedLock.validate(stamp)) { // 檢查樂觀讀鎖後是否有其他寫鎖發生
stamp = stampedLock.readLock(); // 擷取一個悲觀讀鎖
try {
currentX = x;
currentY = y;
            } finally {
stampedLock.unlockRead(stamp); // 釋放悲觀讀鎖
            }
        }
return Math.sqrt(currentX * currentX + currentY * currentY);
    }
}
      

和​

​ReadWriteLock​

​相比,寫入的加鎖是完全一樣的,不同的是讀取。注意到首先我們通過​

​tryOptimisticRead()​

​擷取一個樂觀讀鎖,并傳回版本号。接着進行讀取,讀取完成後,我們通過​

​validate()​

​去驗證版本号,如果在讀取過程中沒有寫入,版本号不變,驗證成功,我們就可以放心地繼續後續操作。如果在讀取過程中有寫入,版本号會發生變化,驗證将失敗。在失敗的時候,我們再通過擷取悲觀讀鎖再次讀取。由于寫入的機率不高,程式在絕大部分情況下可以通過樂觀讀鎖擷取資料,極少數情況下使用悲觀讀鎖擷取資料。

可見,​

​StampedLock​

​把讀鎖細分為樂觀讀和悲觀讀,能進一步提升并發效率。但這也是有代價的:一是代碼更加複雜,二是​

​StampedLock​

​是不可重入鎖,不能在一個線程中反複擷取同一個鎖。

​StampedLock​

​還提供了更複雜的将悲觀讀鎖更新為寫鎖的功能,它主要使用在​

​if-then-update​

​的場景:即先讀,如果讀的資料滿足條件,就傳回,如果讀的資料不滿足條件,再嘗試寫。

小結

​StampedLock​

​提供了樂觀讀鎖,可取代​

​ReadWriteLock​

​以進一步提升并發性能;