Java ReentrantReadWriteLocks - 如何在读锁定中安全地获取写锁定?

我目前正在代码中使用一个 ReentrantReadWriteLock 来同步对树状结构的访问。这个结构很大,可以同时由许多线程读取,偶尔会对它的小部分进行修改 - 所以它似乎很适合读写习语。我知道,对于这个特定的类,人们不能将读锁提升为写锁,因此根据Javadocs,必须在获得写锁之前释放读锁。我以前在非重入上下文中成功使用此模式。

然而,我发现我无法可靠地获取写锁定而不会永远阻塞。由于读锁定是可重入的,并且我实际上正在使用它,因此简单的代码

lock.getReadLock().unlock();
lock.getWriteLock().lock()

如果我已重新进入读取锁,则可以阻止。每次调用解锁只会减少保留计数,并且只有在保留计数达到零时才会实际释放锁定。

EDIT来澄清这一点,因为我认为我最初解释得不太好 - 我知道这个类中没有内置的锁升级,我必须简单地释放读锁定并获取写锁定。我的问题是,无论其他线程在做什么,如果它以可重新进入的方式获取它,调用可能不会实际上释放线程对锁的保持,在这种情况下,对的调用将永远阻塞,因为此线程仍然保持读锁定,因此会阻塞自身。getReadLock().unlock()getWriteLock().lock()

例如,此代码片段永远不会到达 println 语句,即使以单线程方式运行,没有其他线程访问锁:

final ReadWriteLock lock = new ReentrantReadWriteLock();
lock.getReadLock().lock();

// In real code we would go call other methods that end up calling back and
// thus locking again
lock.getReadLock().lock();

// Now we do some stuff and realise we need to write so try to escalate the
// lock as per the Javadocs and the above description
lock.getReadLock().unlock(); // Does not actually release the lock
lock.getWriteLock().lock();  // Blocks as some thread (this one!) holds read lock

System.out.println("Will never get here");

所以我问,有没有一个很好的成语来处理这种情况?具体来说,当持有读锁定(可能可重入)的线程发现它需要执行一些写入操作,因此想要“挂起”自己的读锁定以拾取写锁定(根据需要在其他线程上阻塞以释放其对读锁定的保留),然后“拾取”其对读锁定的保持之后处于相同状态?

由于此 ReadWriteLock 实现是专门为重入而设计的,那么当可以重入获取读锁定时,肯定有某种明智的方法可以将读锁定提升为写锁定?这是关键部分,意味着幼稚的方法不起作用。


答案 1

这是一个老问题,但这里有一个问题的解决方案,以及一些背景信息。

正如其他人所指出的,经典的读写器锁(如JDK ReentrantReadWriteLock)本质上不支持将读锁定升级到写锁定,因为这样做很容易发生死锁。

如果您需要在不首先释放读锁定的情况下安全地获取写锁定,那么有一个更好的选择:看看读写更新锁。

我写了一ReentrantReadWrite_Update_Lock,并在Apache 2.0许可证下将其作为开源发布。我还将该方法的详细信息发布到 JSR166 并发利益邮件列表,并且该方法在列表中的成员的一些来回审查中幸存下来。

这种方法非常简单,正如我在并发利益中提到的,这个想法并不是全新的,因为它至少在2000年就在Linux内核邮件列表中讨论过。此外,.Net平台的ReaderWriterLockSlim也支持锁升级。因此,直到现在,这个概念实际上还没有在Java(AFAICT)上实现。

这个想法是除了锁和锁之外,还提供一个更新锁。更新锁是介于读锁和写锁之间的中间类型锁。与写锁定一样,一次只有一个线程可以获取更新锁定。但就像读锁定一样,它允许对持有它的线程进行读取访问,并发对持有常规读锁定的其他线程进行读取访问。关键功能是更新锁可以从其只读状态升级到写锁定,并且这不容易受到死锁的影响,因为只有一个线程可以持有更新锁并一次处于升级状态。

这支持锁升级,此外,在具有读后读访问模式的应用程序中,它比传统的读写器锁更有效,因为它会在较短的时间内阻止读取线程。

网站上提供了用法示例。该库具有100%的测试覆盖率,位于Maven中心。


答案 2

我在这方面取得了一些进展。通过将锁变量显式声明为 a 而不是简单地声明为 a(不太理想,但在这种情况下可能是必要的 evil),我可以调用 getReadHoldCount() 方法。这让我可以获得当前线程的保留次数,因此我可以多次释放读锁定(并在之后重新获取相同的数字)。所以这是有效的,正如一个快速而肮脏的测试所显示的那样:ReentrantReadWriteLockReadWriteLock

final int holdCount = lock.getReadHoldCount();
for (int i = 0; i < holdCount; i++) {
   lock.readLock().unlock();
}
lock.writeLock().lock();
try {
   // Perform modifications
} finally {
   // Downgrade by reacquiring read lock before releasing write lock
   for (int i = 0; i < holdCount; i++) {
      lock.readLock().lock();
   }
   lock.writeLock().unlock();
}

不过,这将是我能做的最好的事情吗?它感觉不是很优雅,我仍然希望有一种方法可以以一种不那么“手动”的方式处理这个问题。


推荐