Java Concurrency #7: JUC — 進階的讀寫鎖 StampedLock

Charlie Lee
Bucketing
Published in
5 min readJul 22, 2020

比ReadWriteLock高效的讀寫鎖,StampedLock

Photo by Natasya Chen on Unsplash

文章架構

  • StampedLock & ReadWriteLock
  • StampedLock 讀鎖的升級
  • 總結

StampedLock & ReadWriteLock

為何說StampedLock是進階的讀寫鎖(ReadWriteLock),回顧ReadWriteLock的介紹,每次要讀或寫的時候都必須上鎖,為了保護讀和寫的資料一致性,但是加鎖是一個重量級的行為(可以看ReadWriteLock背後原始碼做了多少東西才鎖住),如果應用讀與寫的比例是10000:1,那為每個Read行為加鎖可能就不大理智了,JUC也想到了這點所以也提供了StampedLock的API,將讀鎖在細劃分為樂觀讀與悲觀讀,而寫鎖基本上跟ReadWrtieLock的寫鎖沒有差異,以下是Stamped的使用介紹。

  1. 每次上鎖時會回傳一個stamp (不管樂觀讀 / 悲觀讀 / 寫鎖)
  2. 結束時需要根據這個stamp去解鎖
  3. 樂觀 & 悲觀讀的差異:樂觀讀不會鎖住程式邏輯,但是必須要在操作完驗證stamp確保在操作過程中資料沒被更改 / 而悲觀讀就如同一般的讀鎖,會鎖住程式(寫鎖必須等待讀鎖)

為何效能會優於ReadWirteLock?

  1. 針對讀大於寫的邏輯才會真的優於
  2. 因為樂觀鎖不會真實上鎖,若是每次讀都避免了上鎖,那速度自然會快過每次都會上鎖的ReadWriteLock讀寫鎖

接下來我們看一段Java 官方的範例程式 (Source)

void move(double deltaX, double deltaY) { // an exclusively locked method
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() { // A read-only method
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}

上方式官方的範例程式,用來判斷點的距離與移動點

程式碼重點:

  1. 上鎖後都必須接住回傳的long型態stamp
  2. tryOpimisticRead就是所謂的樂觀鎖,在操作後必須再次透過StampedLock的validate方法驗證是否有效,若操作時資料被別得Thread修改會回傳false,程式邏輯就必須真實調用悲觀讀鎖(readLock())將邏輯鎖住
  3. *注意:tryOpimisticRead不會上鎖,所以不須解鎖

進階: 讀鎖升級成寫鎖

StampedLock與ReadWriteLock的最大差異除了將讀鎖分成樂觀與悲觀之外,StampedLock還幫忙寫了讀鎖替換成寫鎖API(不需要在自己寫釋放和加鎖),但不一定每次都會成功所以還是需要有個保護機制(寫鎖可能正在被使用),範例如下 (一樣參考官方範例)

void moveIfAtOrigin(double newX, double newY) { // upgrade
// Could instead start with optimistic, not read mode
long stamp = sl.readLock();
try {
while (x == 0.0 && y == 0.0) {
long ws = sl.tryConvertToWriteLock(stamp);
if (ws != 0L) {
stamp = ws;
x = newX;
y = newY;
break;
}
else {
sl.unlockRead(stamp);
stamp = sl.writeLock();
}
}
} finally {
sl.unlock(stamp);
}
}

程式重點:

  1. 如同程式碼裡的官方註解,因為範例關係所以這裡或許更適合使用樂觀讀
  2. 申請讀鎖後,發現目前的邏輯條件必須要有寫入動作,呼叫tryConverToWriteLock(stamp)將鎖升級
  3. 切記 / 如果寫鎖正在被佔據的話此方法不會block等待,會直接回傳值,所以必須有個else判斷回傳值是否成工作相對應操作
  4. tryConverTowriteLock()方法底層做的事情,如同上面程式碼寫在else的邏輯。

總結

  1. StampedLock與ReadWriteLock的差異在於,將讀鎖細分為樂觀讀與悲觀讀
  2. 樂觀讀不會上鎖,只是在最後操作結束後需要在比對一次拿樂觀鎖時得到的stamp,確保在讀的時候資料沒有被其他Thread寫入
  3. 悲觀讀與寫鎖就如同ReadWriteLock一樣,讀讀相容,讀寫與寫寫互斥
  4. 若是在讀操作數量大於寫操作數量時,會優先選擇StampedLock保護同步邏輯
  5. 而讀寫差不多時則會選擇ReadWriteLock,因為StampedLock對於ReadWriteLock需要多一些資料的判斷(樂觀讀結束還要在Check一次,如果被改過了就代表樂觀讀的操作是白做的)

--

--