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


67

我现在在代码中使用ReentrantReadWriteLock来同步树状结构的访问。这种结构很大,可以被许多线程同时读取,偶尔对其一部分进行修改,因此似乎很适合读写习惯。我知道对于这一特定类,不能将读取锁提升为写入锁,因此根据Javadocs,必须在获得写入锁之前释放读取锁。之前,我已经在非可重入上下文中成功使用了这种模式。

但是,我发现我无法可靠地获取写锁而不会永远阻塞。由于读锁是可重入的,而我实际上是这样使用的,因此简单的代码

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

如果我重新获得了readlock,则可以阻止。每次解锁请求只会减少保留计数,并且只有在保留计数达到零时才实际释放锁定。

编辑以澄清这一点,因为我认为我一开始解释得不太好-我知道此类中没有内置的锁升级,并且我必须简单地释放读取锁并获得写入锁。我的问题是/不管其他线程在做什么,如果重新获得了该调用,则调用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实现是专门设计为可重入的,因此,当可以重入获取锁时,肯定有某种明智的方法可以将读锁提升为写锁?这是至关重要的部分,这意味着幼稚的方法行不通。


您的问题是否出自于试图在执行写入的同一线程上执行不可知数量的读取锁引起的问题?你不能在另一个线程上写吗?
justinhj

Answers:


29

我在这方面取得了一些进展。通过将lock变量显式声明为aReentrantReadWriteLock而不是简单地声明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
此外,我认为getReadHoldCount返回所有线程的计数(尽管文档没有明确说明)。尽管RRWL允许释放其他线程的读锁,但您的第二个for循环正在使用当前线程重新获取所有线程,这超出了您的期望。
奥利维尔,2009年

15
我认为您正在查看的是getRead LOCK Count *(),而不是getRead HOLD Count *()。后者在Javadocs中没有这样的警告,并且会返回当前线程的保持次数(例如,在线程A中锁定两次,然后在线程B中锁定3次,它将在线程A中返回2,而不是5)。
Andrzej Doyle,2009年

1
精确!这几乎使我的两条评论无效:-)
Olivier

1
很酷的解决方案。我将把它放进我的书包中,以备后用。
Mike Clark,2010年

1
您可能会失去树的一致性。进行读锁定是为了确保在使用时不会发生任何变化。同时,您尝试用我们自己的双手改变这种结构。因此,当您继续遍历树时-绝对可以是另一棵树,您将不得不返回到迭代的开始。
porfirion

27

这是一个古老的问题,但这既是该问题的解决方案,又是一些背景信息。

正如其他人指出的那样,经典的读写器锁(例如JDK ReentrantReadWriteLock)本质上不支持将读取锁升级为写入锁,因为这样做容易导致死锁。

如果您需要安全地获取写锁而无需先释放读锁,则有一个更好的选择:改为查看读写更新锁。

我已经编写了ReentrantReadWrite_Update_Lock,并根据Apache 2.0许可在此处将其作为开源发布。我还向JSR166并发兴趣邮件列表发布了该方法的详细信息,并且该方法在该列表中的成员来回反复审查后仍然有效。

这种方法非常简单,正如我对并发性的利益所提到的,因为它至少在Linux内核邮件列表上的讨论可以追溯到当年的想法并不完全是新的2000年同样的.Net平台的ReaderWriterLockSlim支持锁升级也。到目前为止,这个概念实际上还没有在Java(AFAICT)上实现。

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

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

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


仍然保持吗?我看到自述文件已经更新,但是代码已经两年没有改变了。
纳文

1
我是作者,我对这样的指责感到愤慨,即仍然需要一个项目继续工作:)该项目仍在维护中,但是除非有人记录了错误或功能请求,否则别无他法。该库具有完整的测试范围,开发已完成。
npgall

好的,很少看到“完整的” OSS项目。这有可能出现在Java n + 1标准库中吗?似乎很多人都在重新发明这种东西。
纳文

是的我同意。我在并发兴趣列表上询问它是否最终可以包含在JDK中。但是讨论集中在其他方面,没有人对此发表意见。我希望看到它也包括在内。
npgall

这太棒了。这个答案应该获得的票数远远超过12。现在是13。
Mike Nakis

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

如果没有这种能力,实现可序列化事务的唯一方法是一致地读取一堆数据,然后根据读取的内容进行写操作,这是在开始之前预计将需要进行写操作,从而获得对所有在写东西之前先阅读。这是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的值将不等于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>

9

用这种方法根本不可能做到。

您没有读/写锁定,可以将其从读取升级为写入而不会出现问题。例:

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

现在假设,两个线程将进入该函数。(并且您假设是并发的,对吗?否则,您一开始就不会在意锁。)

假设两个线程将开始在同一时间和运行一样快。那意味着,两个都将获得读锁,这是完全合法的。但是,这两个线程最终都将尝试获取写锁定,而它们中的任何一个都将无法获得:各自的其他线程都拥有读锁定!

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



4

您正在寻找的是锁升级,使用标准java.concurrent ReentrantReadWriteLock不可能(至少不能自动实现)。最好的选择是解锁/锁定,然后检查两者之间没有人进行修改。

您试图执行的操作是强制将所有读取锁定都挡开,这不是一个好主意。读锁是有原因的,您不应该写。:)

编辑:
正如Ran Biron指出的那样,如果您的问题是饥饿(读取锁一直被设置和释放,从不降为零),则可以尝试使用公平队列。但是您的问题听起来不像这是您的问题吗?

编辑2:
我现在看到您的问题,您实际上已经在堆栈上获得了多个读锁,并且您想将它们转换为写锁(升级)。实际上,使用JDK实现是不可能的,因为它无法跟踪读取锁的所有者。可能还有其他人持有您看不到的读取锁,并且不知道有多少读取锁属于您的线程,更不用说您当前的调用堆栈了(即您的循环正在杀死所有读取锁,不只是您自己的,所以您的写锁将不会等待任何并发的读取器完成,并且您最终会陷入混乱)

实际上,我也遇到过类似的问题,最终我写了自己的锁,跟踪谁拥有读锁,并将其升级为写锁。尽管这也是写时复制类型的读/写锁定(允许一个写者在阅读器中),但是仍然有些不同。


是的,这就是问题所在。我并不是要强制所有读取锁定,而只能是当前线程持有的锁定。我很高兴获得写锁,以等待其他线程释放其读锁。
Andrzej Doyle

实际上,JDK实现确实可以跟踪谁在某种程度上锁定了读取锁。getReadHoldCount()仅返回当前线程的计数(通过ThreadLocal),因此在这种情况下可以调用正确数量的unlocks()。实际上,我在自己的答案中输入的代码确实有效。
Andrzej Doyle

有趣的是,然后在jdk1.6中对此进行了更改,jdk1.5完全不使用线程本地。
falstro

1
无法将读取锁升级为可以保证成功的写入锁。“解锁/锁定,然后检查之间没有人进行修改”的一种变体对我有用。
Seun Osewa 2010年

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

这将容易出现僵局。如果一个线程位于upgradeToWriteLock()内,而另一个线程拥有读锁,则第一个线程在等待写锁时将处于阻塞状态,但另一个线程将无法调用dropReadLock(),因为这两种方法都是同步的。
TrogDor

2

到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不应在调试/监视/快速失败检测之外使用。


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

不容易。这个有一个访问者模式元素,这意味着我们锁定该节点以进行读取,然后调出其他类,这些类的逻辑可能会也可能不会回调指向同一节点。此外(从更抽象的意义上讲),如果您不能安全地使用锁,为什么还要使锁重入?:-)
Andrzej Doyle,2009年

这个想法是在客户端调用级别获得读取锁。您的其他类可能会指向“内部”方法(可能会受到保护),因为它们知道它们在读锁的上下文中运行。
奥利维尔,2009年

没错,我可以这样做,但是这些方法本质上并不是线程安全的。尽可能地,我不想依赖客户机需要获得具有不正确(但通常显然是正确的)行为的锁(如果不这样做)。
Andrzej Doyle

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

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

正如我在问题中提到的那样,当以可重入方式获取读锁定时(因此必须调用它的次数要大于1的倍数,但是多少倍?),I understand that with this particular class, one cannot elevate a read lock to a write lock, so as per the Javadocs one must release the read lock before obtaining the write lock. 我的担心完全围绕着如何可靠地释放读锁定rwl.readLock().unlock()
Andrzej Doyle 2014年

-1

在ReentrantReadWriteLock上使用“ fair”标志。“公平”表示锁定请求先到先得。您可能会遇到性能下降的情况,因为当您发出“写”请求时,所有后续的“读”请求都将被锁定,即使在先前已存在的读锁仍处于锁定状态时也可以对其进行处理。


1
我不认为您理解我的问题,饥饿在这里不是问题。在单个线程中尝试以下操作:ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); lock.readLock()。lock(); lock.readLock()。lock(); lock.readLock()。unlock(); lock.writeLock()。lock(); 它仍然永远阻塞。
Andrzej Doyle,2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.