70

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

然而,我发现我无法在不永久阻塞的情况下可靠地获取写锁。由于读锁是可重入的,我实际上是这样使用它的,所以简单的代码

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 实现是专门设计为可重入的,当可以重入获取锁时,肯定有一些明智的方法可以将读锁提升为写锁吗?这是关键部分,这意味着天真的方法不起作用。

4

12 回答 12

30

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

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

如果您需要在不首先释放读锁的情况下安全地获取写锁,那么有一个更好的选择:改为查看读-写-更新锁。

我编写了一个 ReentrantReadWrite_Update_Lock,并在此处根据 Apache 2.0 许可将其作为开源发布。我还发布了有关 JSR166并发兴趣邮件列表的方法的详细信息,并且该方法在该列表上的成员反复审查中幸免于难。

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

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

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

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

于 2013-09-13T11:31:54.160 回答
29

我在这方面取得了一点进展。通过将锁变量显式声明为 aReentrantReadWriteLock而不是简单的 a ReadWriteLock(不太理想,但在这种情况下可能是必要的邪恶),我可以调用该getReadHoldCount()方法。这使我可以获取当前线程的保持数,因此我可以多次释放读锁(然后重新获取相同的数量)。所以这是有效的,正如一个快速而肮脏的测试所示:

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

不过,这会是我能做的最好的吗?它感觉不是很优雅,我仍然希望有一种方法可以以一种不那么“手动”的方式来处理它。

于 2009-01-21T10:58:19.697 回答
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");
    }

}

使用 Java 1.6 输出:

read to write test
<blocks indefinitely>
于 2011-01-29T02:02:11.690 回答
11

你想要做的事情根本不可能通过这种方式。

你不能有一个读/写锁,你可以毫无问题地从读升级到写。例子:

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

现在假设,两个线程将进入该函数。(你假设并发,对吧?否则你一开始就不会关心锁......)

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

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

于 2010-04-04T23:45:44.707 回答
6

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

更多信息请访问http://www.javaspecialists.eu/archive/Issue215.html

于 2015-02-24T06:05:47.633 回答
4

您正在寻找的是锁升级,并且使用标准 java.concurrent ReentrantReadWriteLock 是不可能的(至少不是原子的)。你最好的办法是解锁/锁定,然后检查没有人在两者之间进行修改。

您正在尝试做的事情,强制所有读取锁定不是一个好主意。读锁的存在是有原因的,你不应该写。:)

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

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

我实际上遇到了类似的问题,我最终编写了自己的锁来跟踪谁拥有什么读锁并将这些升级为写锁。虽然这也是一种 Copy-on-Write 类型的读/写锁(允许一个 writer 沿着 reader),所以还是有点不同。

于 2009-01-21T11:01:38.283 回答
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。

于 2010-03-21T13:30:10.290 回答
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();
        }
    }
}
于 2014-07-11T07:55:31.893 回答
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);
  }
}
于 2009-01-21T12:15:46.183 回答
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();
   }
}
于 2011-12-02T20:52:12.363 回答
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();
   }
 }
于 2014-07-02T15:07:46.817 回答
-1

在 ReentrantReadWriteLock 上使用“公平”标志。“公平”意味着锁定请求按照先到先得的方式提供服务。您可能会遇到性能下降,因为当您发出“写入”请求时,所有后续“读取”请求都将被锁定,即使它们本可以在预先存在的读取锁仍被锁定时得到处理。

于 2009-01-21T10:59:18.117 回答