我正在我的代码中使用ReentrantReadWriteLock来同步对树状结构的访问。这个结构很大,并且很多线程一次读取,偶尔会对它的一小部分进行修改 - 所以它似乎很适合读写习惯用法。我理解,对于这个特定的类,不能将读锁提升到写锁,因此根据Javadoc,必须在获得写锁之前释放读锁。我之前在非重入上下文中成功使用了这种模式。
然而,我发现我无法永久地阻止写入锁定。由于读锁是可重入的,我实际上正在使用它,简单的代码
lock.getReadLock().unlock();
lock.getWriteLock().lock()
如果我已经重新获得了readlock,可以阻止。每次解锁调用只会减少保持计数,并且仅在保持计数达到零时才实际释放锁定。
编辑澄清这一点,因为我认为我最初没有解释得太清楚 - 我知道这个类中没有内置锁升级,而且我必须简单释放读锁并获取写锁。我的问题是,无论其他线程正在做什么,调用getReadLock().unlock()
实际上可能不会释放此线程对锁的保持,如果它重新获取它,在这种情况下调用{ <1}}将永远阻塞,因为此线程仍然保持读取锁定并因此阻塞自身。
例如,此代码段永远不会到达println语句,即使在没有其他线程访问锁的情况下运行单线程时也是如此:
getWriteLock().lock()
所以我问,有一个很好的习惯用法来处理这种情况吗?具体来说,当一个持有读锁定的线程(可能是重新发生)发现它需要进行一些写操作,因此想要“挂起”它自己的读锁定以获取写锁定(在其他线程上需要阻塞)释放他们在读锁定上的保留,然后“拿起”它在同一状态下对读锁的保持?
由于这个ReadWriteLock实现是专门设计为可重入的,因此当可以重新获取锁时,肯定有一些明智的方法可以将读锁升级为写锁吗?这是关键部分,意味着天真的方法不起作用。
答案 0 :(得分:27)
我在这方面取得了一些进展。通过将锁变量明确地声明为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();
}
不过,这是否是我能做的最好的事情?它感觉不是很优雅,我仍然希望有一种方法可以用较少的“手动”方式处理它。
答案 1 :(得分:25)
你想做什么应该是可能的。问题是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
<blocks indefinitely>
答案 2 :(得分:21)
这是一个老问题,但这里既是问题的解决方案,也是一些背景信息。
正如其他人所指出的那样,经典readers-writer lock(如JDK ReentrantReadWriteLock)本身不支持将读锁升级到写锁,因为这样做很容易出现死锁。
如果您需要在没有首先发布读锁定的情况下安全地获取写锁定,那么有一个更好的选择:看看读写 - 更新 改为锁定。
我编写了一个ReentrantReadWrite_Update_Lock,并在Apache 2.0许可here下将其作为开源发布。我还发布了JSR166 concurrency-interest mailing list方法的详细信息,该方法在该列表中的成员进行了一些反复审查。
这种方法非常简单,正如我在并发兴趣中提到的那样,这个想法并不是全新的,因为至少在2000年之前就已经在Linux内核邮件列表中进行了讨论。同样是.Net平台的{ {3}}也支持锁升级。因此,直到现在,这个概念还没有在Java(AFAICT)上实现。
除了读取锁定和写入锁定之外,我们的想法是提供更新锁定。更新锁是读锁和写锁之间的中间锁类型。与写锁一样,一次只能有一个线程获取更新锁。但是像读锁一样,它允许对保存它的线程进行读访问,并且同时与其他持有常规读锁的线程进行读访问。关键特性是更新锁可以从其只读状态升级到写锁,并且这不容易发生死锁,因为只有一个线程可以保存更新锁并且可以一次升级。
这支持锁升级,而且在具有 read-before-write 访问模式的应用程序中,它比传统的读写器锁更有效,因为它可以在更短的时间内阻止读取线程。
ReaderWriterLockSlim提供了示例用法。该库具有100%的测试覆盖率,位于Maven中心。
答案 3 :(得分:8)
你想做的事情就是不可能这样。
您不能拥有可以在没有问题的情况下从读取升级到写入的读/写锁定。例如:
void test() {
lock.readLock().lock();
...
if ( ... ) {
lock.writeLock.lock();
...
lock.writeLock.unlock();
}
lock.readLock().unlock();
}
现在假设,两个线程将进入该功能。 (而且你假设是并发的,对吧?否则你不会首先关心锁......)
假设两个线程同时启动 并同样快速地运行 。这意味着,两者都会获得一个读锁定,这是完全合法的。然而,两者最终都会尝试获取写锁定,其中任何一个都将获得:相应的其他线程持有读锁定!
根据定义,允许将读锁升级为写锁的锁容易出现死锁。抱歉,您需要修改方法。
答案 4 :(得分:4)
您正在寻找的是锁升级,并且使用标准的java.concurrent ReentrantReadWriteLock是不可能的(至少不是原子的)。你最好的镜头是解锁/锁定,然后检查没有人在中间进行修改。
你正在尝试做什么,强制所有读锁定不是一个好主意。读锁是有原因的,你不应该写。 :)
修改:
正如Ran Biron指出的那样,如果你的问题是饥饿(读锁定一直被释放,永远不会降到零),你可以尝试使用公平排队。但你的问题听起来不像是你的问题吗?
编辑2:
我现在看到你的问题,你实际上已经在堆栈上获得了多个读锁,并且你想将它们转换为写锁(升级)。事实上,这对JDK实现来说是不可能的,因为它不会跟踪读锁的所有者。可能有其他人持有您看不到的读锁,并且不知道有多少读锁属于您的线程,更不用说您当前的调用堆栈(即您的循环正在杀死 all 读锁,而不仅仅是你自己的,所以你的写锁不会等待任何并发读者完成,你最终会弄得一团糟)
我实际上遇到了类似的问题,最后我写了自己的锁,跟踪谁有什么读锁并将这些锁升级为写锁。虽然这也是一种Write-on-Write类型的读/写锁(允许一位读者沿着读者),所以它仍然有点不同。
答案 5 :(得分:4)
Java 8现在有一个java.util.concurrent.locks.StampedLock
使用tryConvertToWriteLock(long)
API
答案 6 :(得分:2)
这样的事情怎么样?
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();
}
}
}
答案 7 :(得分:1)
我认为ReentrantLock
的动机是树的递归遍历:
public void doSomething(Node node) {
// Acquire reentrant lock
... // Do something, possibly acquire write lock
for (Node child : node.childs) {
doSomething(child);
}
// Release reentrant lock
}
难道你不能重构你的代码来将锁处理移到递归之外吗?
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);
}
}
答案 8 :(得分:1)
到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()
}
}
在写入锁定中,因为任何自尊并发程序都会检查所需的状态。
不应在调试/监视/快速失败检测之外使用HoldCount。
答案 9 :(得分:1)
那么,我们是否期望java只有在此线程尚未对readHoldCount做出贡献时才增加读取信号量计数?这意味着不仅仅是维护一个类型为int的ThreadLocal readholdCount,它应该维护Integer类型的ThreadLocal Set(维护当前线程的hasCode)。如果这很好,我建议(至少现在)不要在同一个类中调用多个读取调用,而是使用一个标志来检查当前对象是否已经获得读取锁定。
private volatile boolean alreadyLockedForReading = false;
public void lockForReading(Lock readLock){
if(!alreadyLockedForReading){
lock.getReadLock().lock();
}
}
答案 10 :(得分:0)
在ReentrantReadWriteLock的文档中找到。它清楚地说,读取器线程在尝试获取写锁时永远不会成功。您尝试实现的目标根本不受支持。在获取写锁之前,您必须释放读锁定。降级仍有可能。
重入
此锁允许读取器和写入器重新读取或写入 锁定{@link ReentrantLock}的风格。不可重入的读者 在写作线程持有的所有写锁之前都不允许 已被释放。
此外,编写器可以获取读锁定,但反之亦然。 在其他应用程序中,写入锁定时重入可能很有用 在调用或回调执行读取的方法期间保持 读锁。 如果读者试图获取写锁定,则永远不会 成功。强>
以上来源的示例用法:
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();
}
}
答案 11 :(得分:-1)
使用ReentrantReadWriteLock上的“fair”标志。 “公平”意味着锁定请求是先到先得的。您可能会遇到性能损失,因为当您发出“写入”请求时,所有后续“读取”请求都将被锁定,即使它们在预先存在的读锁仍然被锁定时可能已被服务。