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

在Java 5中,我们获得了ReadWriteLock接口,并带有ReentrantReadWriteLock实现。它具有明智的限制,我们可以将写锁降级为读锁,但不能将读锁升级为写锁。当我们尝试时,我们将立即陷入死锁。出现此限制的原因是,如果两个线程都具有读锁,那么如果两个线程都尝试同时升级怎么办?为了安全起见,它会始终使尝试升级的所有线程陷入死锁。、

降级ReentrantReadWriteLock可以正常工作,在这种情况下,我们可以同时持有读取和写入锁定。降级意味着在持有写锁的同时,我们也锁定了读锁,然后释放了写锁。这意味着我们不允许任何其他线程写入,但它们可以读取。

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

尝试将ReentrantReadWriteLock从读取升级为写入会导致死锁:

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

Kotlin中的ReadWriteLock

让我们看一下Kotlin如何管理ReadWriteLock。

下面是降级代码:

<font><i>// DowngradeDemoKotlin.kt</i></font><font>
<b>import</b> java.util.concurrent.locks.*
<b>import</b> kotlin.concurrent.*

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

下面是升级:

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

竟然没有发生死锁。

如果我们窥视Kotlin扩展功能的实现,ReentrantReadWriteLock.write()将会看到以下内容:

Kotlin的扩展功能ReentrantReadWriteLock.write()通过在升级之前放开读锁来作弊,从而为竞赛条件打开了大门。

/ **

*在此锁的写锁下执行给定的[action]。

*

*如果需要,该功能会从读取锁定升级为写入锁定,

*但是 此升级不是原子升级

因为[ReentrantReadWriteLock] 不支持此类升级。

*为了进行这种升级,此功能首先释放

该线程持有的所有*读锁,然后获取写锁,并且

*释放后再重新获取读锁。

*

*因此,如果已

通过检查某些条件启动了* 写锁

内部的[action] ,则必须在[action]内部重新检查条件*以避免可能的争用。

*

* @return操作的返回值。

* /

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

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

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

原来,Kotlin的扩展功能ReentrantReadWriteLock.write()通过在升级之前放开读锁来作弊,从而为竞争打开了漏洞大门。

使用StampedLock升级

Java 8 StampedLock使我们可以更好地控制应该如何处理失败的升级。StampedLock 不是 可重入的,这意味着我们不能同时持有读取和写入锁。戳记未绑定到特定线程,因此我们也不能同时从一个线程持有两个写锁。我们可以同时持有许多读锁,每个读锁都有不同的标记。但是我们只能得到一个写锁。这是一个演示:

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

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

由于StampedLock不知道哪个线程拥有锁,因此DowngradeDemo会死锁:

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

但是,StampedLock确实允许我们 尝试 升级或降级我们的锁。这还将把戳记转换为新类型。例如,这是我们如何正确进行降级。请注意,我们不需要解锁写锁,因为戳记是从写转换为读的。

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

从读锁升级到写锁的代码:

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

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

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章