ReadWriteLock讀寫鎖升級的踩坑:Kotlin作弊,最好使用StampedLock - javaspecialists

banq發表於2020-05-29

在Java 5中,我們獲得了ReadWriteLock介面,並帶有ReentrantReadWriteLock實現。它具有明智的限制,我們可以將寫鎖降級為讀鎖,但不能將讀鎖升級為寫鎖。當我們嘗試時,我們將立即陷入死鎖。出現此限制的原因是,如果兩個執行緒都具有讀鎖,那麼如果兩個執行緒都嘗試同時升級怎麼辦?為了安全起見,它會始終使嘗試升級的所有執行緒陷入死鎖。、
降級ReentrantReadWriteLock可以正常工作,在這種情況下,我們可以同時持有讀取和寫入鎖定。降級意味著在持有寫鎖的同時,我們也鎖定了讀鎖,然後釋放了寫鎖。這意味著我們不允許任何其他執行緒寫入,但它們可以讀取。

import java.util.concurrent.locks.*;
// This runs through fine
public class DowngradeDemo {
  public static void main(String... args) {
    var rwlock = new ReentrantReadWriteLock();
    System.out.println(rwlock); // w=0, r=0
    rwlock.writeLock().lock();
    System.out.println(rwlock); // w=1, r=0
    rwlock.readLock().lock();
    System.out.println(rwlock); // w=1, r=1
    rwlock.writeLock().unlock();
    // at this point other threads can also acquire read locks
    System.out.println(rwlock); // w=0, r=1
    rwlock.readLock().unlock();
    System.out.println(rwlock); // w=0, r=0
  }
}

嘗試將ReentrantReadWriteLock從讀取升級為寫入會導致死鎖:

// This deadlocks
public class UpgradeDemo {
  public static void main(String... args) {
    var rwlock = new ReentrantReadWriteLock();
    System.out.println(rwlock); // w=0, r=0
    rwlock.readLock().lock();
    System.out.println(rwlock); // w=0, r=1
    rwlock.writeLock().lock(); // deadlock
    System.out.println(rwlock); 
    rwlock.readLock().unlock();
    System.out.println(rwlock);
    rwlock.writeLock().unlock();
    System.out.println(rwlock);
  }
}


Kotlin中的ReadWriteLock
讓我們看一下Kotlin如何管理ReadWriteLock。
下面是降級程式碼:

// DowngradeDemoKotlin.kt
import java.util.concurrent.locks.*
import kotlin.concurrent.*

fun main() {
  val rwlock = ReentrantReadWriteLock()
  println(rwlock) // w=0, r=0
  rwlock.write {
    println(rwlock) // w=1, r=0
    rwlock.read {
      println(rwlock) // w=1, r=1
    }
    println(rwlock) // w=1, r=0
  }
  println(rwlock) // w=0, r=0
}


下面是升級:

// UpgradeDemoKotlin.kt
fun main() {
  val rwlock = ReentrantReadWriteLock()
  println(rwlock) // w=0, r=0
  rwlock.read {
    println(rwlock) // w=0, r=1
    rwlock.write {
      println(rwlock) // w=1, r=0
    }
    println(rwlock) // w=0, r=1
  }
  println(rwlock) // w=0, r=0
}


竟然沒有發生死鎖。
如果我們窺視Kotlin擴充套件功能的實現,ReentrantReadWriteLock.write()將會看到以下內容:

Kotlin的擴充套件功能ReentrantReadWriteLock.write()透過在升級之前放開讀鎖來作弊,從而為競賽條件開啟了大門。

/ ** 
 *在此鎖的寫鎖下執行給定的[action]。
 * 
 *如果需要,該功能會從讀取鎖定升級為寫入鎖定,
 *但是此升級不是原子升級
 因為[ReentrantReadWriteLock] 不支援此類升級。
 *為了進行這種升級,此功能首先釋放
 該執行緒持有的所有*讀鎖,然後獲取寫鎖,並且
 *釋放後再重新獲取讀鎖。
 * 
 *因此,如果已
 透過檢查某些條件啟動了* 寫鎖
 內部的[action] ,則必須在[action]內部重新檢查條件*以避免可能的爭用。
 * 
 * @return操作的返回值。
 * /

@kotlin.internal.InlineOnly
public inline
fun <T> ReentrantReadWriteLock.write(action: () -> T): T {
  val rl = readLock()

  val readCount = if (writeHoldCount == 0) readHoldCount else 0
  repeat(readCount) { rl.unlock() }

  val wl = writeLock()
  wl.lock()
  try {
    return action()
  } finally {
    repeat(readCount) { rl.lock() }
    wl.unlock()
  }
}


原來,Kotlin的擴充套件功能ReentrantReadWriteLock.write()透過在升級之前放開讀鎖來作弊,從而為競爭開啟了漏洞大門。

使用StampedLock升級
Java 8 StampedLock使我們可以更好地控制應該如何處理失敗的升級。StampedLock 不是可重入的,這意味著我們不能同時持有讀取和寫入鎖。戳記未繫結到特定執行緒,因此我們也不能同時從一個執行緒持有兩個寫鎖。我們可以同時持有許多讀鎖,每個讀鎖都有不同的標記。但是我們只能得到一個寫鎖。這是一個演示:

public class StampedLockDemo {
  public static void main(String... args) {
    var sl = new StampedLock();
    var stamps = new ArrayList<Long>();
    System.out.println(sl); // Unlocked
    for (int i = 0; i < 42; i++) {
      stamps.add(sl.readLock());
    }
    System.out.println(sl); // Read-Locks:42
    stamps.forEach(sl::unlockRead);
    System.out.println(sl); // Unlocked

    var stamp1 = sl.writeLock();
    System.out.println(sl); // Write-Locked
    var stamp2 = sl.writeLock(); // deadlocked
    System.out.println(sl); // Not seen...
  }
}

由於StampedLock不知道哪個執行緒擁有鎖,因此DowngradeDemo會死鎖:

public class StampedLockDowngradeFailureDemo {
  public static void main(String... args) {
    var sl = new StampedLock();
    System.out.println(sl); // Unlocked
    long wstamp = sl.writeLock();
    System.out.println(sl); // Write-Locked
    long rstamp = sl.readLock(); // deadlocked
    System.out.println(sl); // Not seen...
  }
}


但是,StampedLock確實允許我們嘗試升級或降級我們的鎖。這還將把戳記轉換為新型別。例如,這是我們如何正確進行降級。請注意,我們不需要解鎖寫鎖,因為戳記是從寫轉換為讀的。

public class StampedLockDowngradeDemo {
  public static void main(String... args) {
    var sl = new StampedLock();
    System.out.println(sl); // Unlocked
    long wstamp = sl.writeLock();
    System.out.println(sl); // Write-locked
    long rstamp = sl.tryConvertToReadLock(wstamp);
    if (rstamp != 0) {
      System.out.println("Converted write to read");
      System.out.println(sl); // Read-locks:1
      sl.unlockRead(rstamp);
      System.out.println(sl); // Unlocked
    } else { // this cannot happen (famous last words)
      sl.unlockWrite(wstamp);
      throw new AssertionError("Failed to downgrade lock");
    }
  }
}


從讀鎖升級到寫鎖的程式碼:

public class StampedLockUpgradeDemo {
  public static void main(String... args) {
    var sl = new StampedLock();
    System.out.println(sl); // Unlocked
    long rstamp = sl.readLock();
    System.out.println(sl); // Read-locks:1
    long wstamp = sl.tryConvertToWriteLock(rstamp);
    if (wstamp != 0) {
      // works if no one else has a read-lock
      System.out.println("Converted read to write");
      System.out.println(sl); // Write-locked
      sl.unlockWrite(wstamp);
    } else {
      // we do not have an exclusive hold on read-lock
      System.out.println("Could not convert read to write");
      sl.unlockRead(rstamp);
    }
    System.out.println(sl); // Unlocked
  }
}

與Kotlin ReentrantReadWriteLock.write()擴充套件功能不同,這將自動進行轉換。但是,它仍然可能失敗,例如,如果另一個執行緒當前也持有讀取鎖。在這種情況下,一種合理的方法是跳出並重試,或者以寫鎖定開始。
 

相關文章