如何安全地获取写锁定?

我在我的代码中使用ReentrantReadWriteLock来同步树状结构的访问。 这个结构很大,可以一次读取多个线程,偶尔修改它的一小部分 - 所以它似乎很适合读写的习惯用法。 我明白,对于这个特定的类,不能提升读写锁,因为每个Javadocs必须在获得写锁之前释放读锁。 我以前在非重入上下文中成功使用了此模式。

然而我发现,我无法可靠地获得写入锁而不会永久阻止。 由于读锁是可重入的,我实际上是这样使用它,简单的代码

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

如果我已经重新获得了读锁,可以阻止它。 每次解锁呼叫都会减少保持计数,并且只有当保持计数达到零时锁定才会释放。

编辑澄清这一点,因为我不认为我最初解释得太好 - 我知道这个类没有内置的锁升级,我只需要释放读锁并获得写锁。 我的问题是,不管其他线程在做什么,调用getReadLock().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实现是专门设计为可重入的,当锁可能被重复获取时,肯定有一些明智的方法可以将读锁提升为写锁。 这是意味着幼稚方法无效的关键部分。


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

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

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


你想做什么应该是可能的。 问题是Java不提供可以将读锁升级到写锁的实现。 具体来说,javadocReentrantReadWriteLock表示它不允许从读锁升级到写锁。

无论如何,Jakob Jenkov描述了如何实现它。 有关详细信息,请参阅http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade。

为什么升级读取到写入锁是必要的

从读取到写入锁定的升级是有效的(尽管在其他答案中有相反的断言)。 死锁可能发生,因此实现的一部分是通过在线程中抛出异常来破坏死锁来识别死锁并将其分解的代码。 这意味着作为事务的一部分,您必须处理DeadlockException,例如,通过再次完成该工作。 典型的模式是:

boolean repeat;
do {
  repeat = false;
  try {
   readSomeStuff();
   writeSomeStuff();
   maybeReadSomeMoreStuff();
  } catch (DeadlockException) {
   repeat = true;
  }
} while (repeat);

没有这种能力,实现可序列化事务的唯一方法就是一致地读取大量数据,然后根据读取的内容写入内容,这是为了在开始之前预测写入操作是必需的,因此需要对所有数据在写入需要写入的内容之前阅读。 这是Oracle使用的KLUDGE(SELECT FOR UPDATE ...)。 此外,它实际上降低了并发性,因为在事务正在运行时,没有其他人可以读取或写入任何数据!

特别是,在获取写入锁定之前释放读取锁定会产生不一致的结果。 考虑:

int x = someMethod();
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

您必须知道someMethod()或它调用的任何方法是否会在y上创建可重入读锁定! 假设你知道它的确如此。 那么如果你先释放读锁,

int x = someMethod();
y.readLock().unlock();
// problem here!
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

另一个线程可能会在您释放其读取锁之后以及在获取写入锁之前更改y。 所以y的价值不会等于x。

测试代码:将读锁升级到写锁块:

import java.util.*;
import java.util.concurrent.locks.*;

public class UpgradeTest {

    public static void main(String[] args) 
    {   
        System.out.println("read to write test");
        ReadWriteLock lock = new ReentrantReadWriteLock();

        lock.readLock().lock(); // get our own read lock
        lock.writeLock().lock(); // upgrade to write lock
        System.out.println("passed");
    }

}

使用Java 1.6输出:

read to write test
<blocks indefinitely>

这是一个古老的问题,但这里既是解决问题的办法,也是一些背景信息。

正如其他人指出的那样,经典的读写器锁(如JDK ReentrantReadWriteLock)固有地不支持将读锁升级到写锁,因为这样做容易造成死锁。

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

我已经写了一个ReentrantReadWrite_Update_Lock,并在此处以Apache 2.0许可证的形式将它作为开源发布。 我还向JSR166并发利益邮件列表发布了这个方法的详细信息,并且该方法中的成员来回审查了该方法。

这个方法非常简单,正如我在并发兴趣中提到的那样,这个想法并不是全新的,至少早在2000年就已经在Linux内核邮件列表中讨论过了。另外.Net平台的ReaderWriterLockSlim支持锁升级也。 到目前为止,这个概念实际上还没有在Java(AFAICT)上实现。

这个想法是除了读锁和写锁之外还提供更新锁。 更新锁是读锁和写锁之间的中间类型的锁。 与写入锁一样,一次只有一个线程可以获取更新锁。 但是就像读锁一样,它允许对持有它的线程进行读访问,并且同时对其他持有常规读锁的线程进行访问。 关键特性是更新锁可以从其只读状态升级到写入锁,并且不会因死锁而受到影响,因为只有一个线程可以保存更新锁,并且一次可以升级。

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

在网站上提供了示例用法。 图书馆有100%的测试覆盖率,位于Maven中心。

链接地址: http://www.djcxy.com/p/41801.html

上一篇: how to safely acquire write lock?

下一篇: What is the equivalent of the C++ Pair<L,R> in Java?