我现在在我的代码中使用ReentrantReadWriteLock来同步对树状结构的访问.这个结构很大,并且很多线程一次读取,偶尔会对它的一小部分进行修改 - 所以它似乎很适合读写习惯用法.我理解,对于这个特定的类,不能将读锁提升到写锁,因此根据Javadoc,必须在获得写锁之前释放读锁.我以前在非重入上下文中成功使用了这种模式.
然而,我发现我无法永久地阻止写入锁定.由于读锁是可重入的,我实际上正在使用它,简单的代码
lock.getReadLock().unlock(); lock.getWriteLock().lock()
如果我已经重新获得了重新锁定,则可以阻止.每次解锁调用只会减少保持计数,并且仅在保持计数达到零时才实际释放锁定.
编辑澄清这一点,因为我认为我最初没有解释得太清楚 - 我知道这个类中没有内置的锁升级,我必须简单地释放读锁并获得写锁.我的问题是,无论其他线程正在做什么,如果它重新获取它,调用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实现是专门设计为可重入的,因此当可以重新获取锁时,肯定有一些明智的方法可以将读锁升级到写锁?这是关键部分,这意味着天真的方法不起作用.
我在这方面取得了一些进展.通过明确地将锁变量声明为a 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没有提供可以将读锁升级为写锁的实现.具体来说,javadoc ReentrantReadWriteLock表示它不允许从读锁升级到写锁.
无论如何,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);
如果没有这种能力,实现可序列化事务的唯一方法是一致地读取一堆数据,然后根据读取的内容写入内容,这是预期在开始之前写入是必要的,因此在所有数据上获取WRITE锁定.在写下需要写的内容之前阅读.这是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"); } }
read to write test
这是一个老问题,但这里既是问题的解决方案,也是一些背景信息.
正如其他人所指出的那样,经典的读写器锁(如JDK ReentrantReadWriteLock)本身不支持将读锁升级到写锁,因为这样做很容易出现死锁.
如果你需要在没有首先释放读锁的情况下安全地获取写锁,那么有一个更好的选择:看一下读写更新锁.
我写了一个ReentrantReadWrite_Update_Lock,并在这里以Apache 2.0许可证的形式发布它作为开源.我还发布了JSR166 并发兴趣邮件列表的方法的详细信息,并且该方法在该列表上的成员进行了一些来回的审查.
这种方法非常简单,正如我在并发兴趣方面提到的那样,这个想法并不是全新的,因为它至少在2000年左右在Linux内核邮件列表中进行过讨论.此外,.Net平台的ReaderWriterLockSlim支持锁升级也.因此,直到现在,这个概念还没有在Java(AFAICT)上实现.
这个想法是除了读锁和写锁之外还提供更新锁.更新锁是读锁和写锁之间的中间锁类型.与写锁一样,一次只能有一个线程获取更新锁.但是像读锁一样,它允许对保存它的线程进行读访问,并且同时允许其他持有常规读锁的线程.关键特性是更新锁可以从其只读状态升级到写锁,并且这不容易发生死锁,因为只有一个线程可以保存更新锁并且可以一次升级.
这支持锁升级,而且在具有读前写访问模式的应用程序中,它比传统的读写器锁更有效,因为它可以在更短的时间内阻止读取线程.
网站上提供了示例用法.该库具有100%的测试覆盖率,位于Maven中心.
你想要做的就是这样做是不可能的.
您不能拥有可以从读取到写入升级的读/写锁定而不会出现问题.例:
void test() { lock.readLock().lock(); ... if ( ... ) { lock.writeLock.lock(); ... lock.writeLock.unlock(); } lock.readLock().unlock(); }
现在假设,两个线程将进入该功能.(而且你假设是并发的,对吧?否则你首先不会关心锁......)
假设两个线程同时启动并且运行速度相同.这意味着,两者都会获得一个读锁定,这是完全合法的.但是,两者最终都会尝试获取写锁定,它们中的任何一个都将获得:相应的其他线程持有读锁定!
根据定义,允许将读锁升级为写锁的锁容易出现死锁.抱歉,您需要修改您的方法.