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