当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《JavaReentrantReadWriteLocks-如何安全地获取写锁?》经验,为你挑选了4个好方法。

我现在在我的代码中使用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实现是专门设计为可重入的,因此当可以重新获取锁时,肯定有一些明智的方法可以将读锁升级到写锁?这是关键部分,这意味着天真的方法不起作用.



1> Andrzej Doyl..:

我在这方面取得了一些进展.通过明确地将锁变量声明为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();
}

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


我想你正在看getRead*LOCK*Count*(),而不是getRead*HOLD*Count*().后者在Javadocs中没有这样的警告并且确实返回当前线程的保持数(例如,在线程A中锁定两次,然后在线程B中锁定3次,它将在线程A中返回2而不是5).

2> Bob..:

你想做什么应该是可能的.问题是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");
    }

}

使用Java 1.6输出:

read to write test



3> npgall..:

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

正如其他人所指出的那样,经典的读写器锁(如JDK ReentrantReadWriteLock)本身不支持将读锁升级到写锁,因为这样做很容易出现死锁.

如果你需要在没有首先释放读锁的情况下安全地获取写锁,那么有一个更好的选择:看一下读写更新锁.

我写了一个ReentrantReadWrite_Update_Lock,并在这里以Apache 2.0许可证的形式发布它作为开源.我还发布了JSR166 并发兴趣邮件列表的方法的详细信息,并且该方法在该列表上的成员进行了一些来回的审查.

这种方法非常简单,正如我在并发兴趣方面提到的那样,这个想法并不是全新的,因为它至少在2000年左右在Linux内核邮件列表中进行过讨论.此外,.Net平台的ReaderWriterLockSlim支持锁升级也.因此,直到现在,这个概念还没有在Java(AFAICT)上实现.

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

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

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



4> Steffen Heil..:

你想要做的就是这样做是不可能的.

您不能拥有可以从读取到写入升级的读/写锁定而不会出现问题.例:

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

现在假设,两个线程将进入该功能.(而且你假设是并发的,对吧?否则你首先不会关心锁......)

假设两个线程同时启动并且运行速度相同.这意味着,两者都会获得一个读锁定,这是完全合法的.但是,两者最终都会尝试获取写锁定,它们中的任何一个都将获得:相应的其他线程持有读锁定!

根据定义,允许将读锁升级为写锁的锁容易出现死锁.抱歉,您需要修改您的方法.

推荐阅读
mobiledu2402851173
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有