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

新手上路,请多包涵

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

然而,我发现的是,我无法在不永远阻塞的情况下可靠地获取写锁。由于读锁是可重入的,我实际上是这样使用它的,简单的代码

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

如果我以可重入方式获取了读锁,则可以阻止。每次调用 unlock 只会减少保持计数,只有当保持计数为零时才会真正释放锁。

编辑 以澄清这一点,因为我认为我最初解释得不太好——我知道这个类中没有内置的锁升级,我必须简单地释放读锁并获得写锁。我的问题是/是无论其他线程在做什么,调用 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 实现是专门为可重入而设计的,因此当可能以可重入方式获取锁时,肯定有一些明智的方法将读锁提升为写锁吗?这是关键部分,意味着天真方法不起作用。

原文由 Andrzej Doyle 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 572
2 个回答

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

 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();
}

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

原文由 Andrzej Doyle 发布,翻译遵循 CC BY-SA 4.0 许可协议

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

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

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

我编写了一个 ReentrantReadWrite_Update_Lock,并在 此处 以 Apache 2.0 许可将其作为开源发布。我还在 JSR166 concurrency-interest 邮件列表中 发布了该方法的详细信息,该方法经受住了该列表中成员的反复审查。

该方法非常简单,正如我在 concurrency-interest 中提到的,这个想法并不是全新的,因为它至少早在 2000 年就在 Linux 内核邮件列表上进行了讨论。此外,.Net 平台的 ReaderWriterLockSlim 支持锁升级还。如此有效,这个概念直到现在才在 Java (AFAICT) 上实现。

思路是在 锁和 锁之外,再提供一个 更新 锁。更新锁是介于读锁和写锁之间的一种中间类型的锁。和写锁一样,一次只有一个线程可以获取更新锁。但就像读锁一样,它允许对持有它的线程进行读访问,并同时允许对持有常规读锁的其他线程进行读访问。关键特性是更新锁可以从其只读状态升级为写锁,这不容易发生死锁,因为一次只有一个线程可以持有更新锁并处于升级状态。

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

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

原文由 npgall 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题