Java ReentrantReadWriteLocks – 如何安全地获取写locking?

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

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

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

可以阻止,如果我已经获得了readlock reentntly。 每次解锁的呼叫都会减less保持计数,并且只有当保持计数达到零时locking才被释放。

编辑澄清这一点,因为我不认为我最初解释得太好 – 我知道这个类没有内置的锁升级,我不得不简单地释放读锁,并获得写锁。 我的问题是/不pipe其他线程在做什么,调用getReadLock().unlock()可能实际上并不释放这个线程的锁,如果它获得了reentrantly,在这种情况下调用getWriteLock().lock()会永久阻塞,因为这个线程仍然持有读锁,从而阻塞自己。

例如,即使在没有其他线程访问锁的情况下运行singlethreaded,该代码片段也将永远不会到达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实现是专门devise为可重入的,当锁可以被重复获取时,肯定有一些合理的方法来提升读锁到写locking? 这是意味着幼稚的方法不起作用的关键部分。

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

 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没有提供一个可以升级读锁来写锁的实现。 具体来说,javadoc ReentrantReadWriteLock说,它不允许从读锁升级到写锁。

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

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

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

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

如果没有这种能力,实现一个序列化事务的唯一方法就是一致地读取一堆数据,然后根据读取的内容来写一些事情,那么在开始之前就需要写入,因此对所有数据在写之前先阅读需要写的东西。 这是Oracle使用的KLUDGE(SELECT FOR UPDATE …)。 此外,它实际上降低了并发性,因为在事务运行时,没有其他人可以读取或写入任何数据。

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

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

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

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

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

testing代码:将读锁升级到写锁块:

 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 )固有地不支持将读锁升级到写锁,因为这样做容易造成死锁。

如果您需要安全地获取写锁,而不先释放读锁,则有一个更好的select:请改为读写更新锁。

我已经写了一个ReentrantReadWrite_Update_Lock,并在这里以Apache 2.0许可证的forms作为开源发布。 我还向JSR166 并发利益邮件列表发布了这个方法的详细信息,而且这个方法的成员在这个列表中反复审查。

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

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

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

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

你试图做的事情是不可能的。

你不能有读/写锁,你可以从读取升级没有问题。 例:

 void test() { lock.readLock().lock(); ... if ( ... ) { lock.writeLock.lock(); ... lock.writeLock.unlock(); } lock.readLock().unlock(); } 

现在假设,两个线程将进入该function。 (而且你假设并发,对吧?否则你不会在意第一个锁在…)

假设两个线程同时启动并运行同样快 。 这意味着,两者都将获得读锁,这是完全合法的。 然而,那么最终都会尝试获取写入锁,而这些写入锁都不会得到:相应的其他线程持有读取锁!

根据定义,允许将读锁升级到写锁的locking很容易发生死锁。 对不起,但你需要修改你的方法。

你正在寻找的是一个锁升级,并不可能(至less不是primefaces)使用标准的java.concurrent ReentrantReadWriteLock。 你最好的镜头是解锁/locking,然后检查没有人在中间进行修改。

你试图做的,强制所有的读取locking不是一个好主意。 读锁是有原因的,你不应该写。 🙂

编辑:
正如Ran Biron所指出的那样,如果你的问题是饥饿(读锁一直在被设置和释放,永远不会降到零),你可以尝试使用公平的排队。 但是你的问题听起来不像是你的问题?

编辑2:
我现在看到你的问题,你实际上已经获得了堆栈上的多个读锁,并且你想把它们转换成写锁(升级)。 事实上,JDK实现是不可能的,因为它不会跟踪读锁的所有者。 可能有其他人持有你不会看到的读锁,也不知道有多less读锁属于你的线程,更不用提你当前的调用栈了(即你的循环正在终止所有的读锁,不仅仅是你自己的,所以你的写锁不会等待任何并发的读者完成,而且最终你的手会一团糟)

实际上我遇到了一个类似的问题,最后我写了一个自己的锁,logging了读取锁的人,并将其升级为写入锁。 虽然这也是一种写/写复制锁(允许一个作者沿读者),所以它仍然有点不同。

Java 8现在有一个带有tryConvertToWriteLock(long) API的java.util.concurrent.locks.StampedLock

更多信息在http://www.javaspecialists.eu/archive/Issue215.html

那这样的事情呢?

 class CachedData { Object data; volatile boolean cacheValid; private class MyRWLock { private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public synchronized void getReadLock() { rwl.readLock().lock(); } public synchronized void upgradeToWriteLock() { rwl.readLock().unlock(); rwl.writeLock().lock(); } public synchronized void downgradeToReadLock() { rwl.writeLock().unlock(); rwl.readLock().lock(); } public synchronized void dropReadLock() { rwl.readLock().unlock(); } } private MyRWLock myRWLock = new MyRWLock(); void processCachedData() { myRWLock.getReadLock(); try { if (!cacheValid) { myRWLock.upgradeToWriteLock(); try { // Recheck state because another thread might have acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } } finally { myRWLock.downgradeToReadLock(); } } use(data); } finally { myRWLock.dropReadLock(); } } } 

我猜想ReentrantLock是由树的recursion遍历激发的:

 public void doSomething(Node node) { // Acquire reentrant lock ... // Do something, possibly acquire write lock for (Node child : node.childs) { doSomething(child); } // Release reentrant lock } 

你不能重构你的代码来移动recursion之外的锁处理吗?

 public void doSomething(Node node) { // Acquire NON-reentrant read lock recurseDoSomething(node); // Release NON-reentrant read lock } private void recurseDoSomething(Node node) { ... // Do something, possibly acquire write lock for (Node child : node.childs) { recurseDoSomething(child); } } 

那么,我们是否期望java只有在这个线程还没有贡献给readHoldCount时才会增加读信号计数呢? 这意味着不像维护一个types为int的ThreadLocal readholdCount,它应该维护Integertypes的ThreadLocal Set(维护当前线程的hasCode)。 如果这没问题,我会build议(至less现在)不要在同一个类中调用多个读取调用,而是使用一个标志来检查读取锁是否已经被当前对象获得。

 private volatile boolean alreadyLockedForReading = false; public void lockForReading(Lock readLock){ if(!alreadyLockedForReading){ lock.getReadLock().lock(); } } 

到OP:就像你进入锁一样多次解锁,简单如此:

 boolean needWrite = false; readLock.lock() try{ needWrite = checkState(); }finally{ readLock().unlock() } //the state is free to change right here, but not likely //see who has handled it under the write lock, if need be if (needWrite){ writeLock().lock(); try{ if (checkState()){//check again under the exclusive write lock //modify state } }finally{ writeLock.unlock() } } 

在任何自重并发程序的写锁中检查所需的状态。

除了debugging/监控/快速失败检测之外,不应使用HoldCount。

在ReentrantReadWriteLock的文档中find。 它清楚地表明,读取器线程在尝试获取写入locking时永远不会成功。 你试图达到什么是根本不支持。 在获取写锁之前,您必须释放读锁。 降级仍然是可能的。

重入

此锁允许读者和作者以“连接ReentrantLock”样式重新获取读取或写入locking。 直到写入线程持有的所有写入locking都被释放,才允许不可重入的读取器。

另外,作者可以获取读锁,但是反之亦然。 在其他应用程序中,当在执行读取locking下执行读取的方法的调用或callback期间保持写入locking时,重新进入可能非常有用。 如果读者试图获得写入锁,它将永远不会成功。

来自以上来源的示例用法:

  class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); // Recheck state because another thread might have acquired // write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); rwl.writeLock().unlock(); // Unlock write, still hold read } use(data); rwl.readLock().unlock(); } } 

使用ReentrantReadWriteLock上的“公平”标志。 “公平”意味着locking请求先到先得。 您可能会遇到性能破坏,因为当您发出“写入”请求时,所有后续的“读取”请求都将被locking,即使在预先存在的读取锁仍处于locking状态时它们可能已被提供服务。