1

ReadWriteLock读写锁升级的踩坑:Kotlin作弊,最好使用StampedLock - javaspecialists

 4 months ago
source link: https://www.jdon.com/54287.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ReadWriteLock读写锁升级的踩坑:Kotlin作弊,最好使用StampedLock - javaspecialists - 极道

在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()扩展功能不同,这将自动进行转换。但是,它仍然可能失败,例如,如果另一个线程当前也持有读取锁。在这种情况下,一种合理的方法是跳出并重试,或者以写锁定开始。
 


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK