【问题标题】:Java ReentrantReadWriteLocks - how to safely acquire write lock when in a read lock?Java ReentrantReadWriteLocks - 在读锁中如何安全地获取写锁?
【发布时间】:2010-10-02 15:53:26
【问题描述】:

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

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

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

如果我以可重入方式获得了读锁,则可以阻止。每次解锁调用只会减少保持计数,并且只有在保持计数达到零时才会真正释放锁。

EDIT 澄清这一点,因为我认为我最初解释得不是很好 - 我知道这个类中没有内置的锁升级,我必须简单地释放读锁并获得写锁。我的问题是/是不管其他线程在做什么,调用 getReadLock().unlock() 可能实际上不会释放 this 线程对锁的持有,如果它重新获得它,在这种情况下调用getWriteLock().lock()将永远阻塞,因为该线程仍然持有读锁并因此阻塞自己。

例如,这段代码 sn-p 永远不会到达 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 实现是专门设计为可重入的,那么当可以重入获取锁时,肯定有一些明智的方法可以将读锁提升为写锁吗?这是关键部分,这意味着幼稚的方法不起作用。

【问题讨论】:

  • 您的问题是否来自尝试在您正在写入的同一线程上执行不可知数量的读取锁定。你不能在另一个线程上写吗?

标签: java concurrency reentrantreadwritelock


【解决方案1】:

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

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

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

我编写了一个 ReentrantReadWrite_Update_Lock,并在 Apache 2.0 许可 here 下将其作为开源发布。我还发布了 JSR166 concurrency-interest mailing list 方法的详细信息,该方法经受住了该列表中成员的反复审查。

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

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

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

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

【讨论】:

  • 这还在维护吗?我看到自述文件已经更新,但代码已经 2 年没有改变了。
  • 我是作者,我对仍然有效的项目需要持续关注的指责表示不满 :) 该项目仍在维护,但除非有人记录错误或功能请求,否则没有什么可做的了完成。该库具有完整的测试覆盖率和开发完成。
  • 啊,好的,很少见到“完整”的 OSS 项目。这有没有可能出现在 Java n+1 标准库中?这似乎是很多人重新发明的那种东西。
  • 是的,我同意。我在并发兴趣列表上询问它是否最终可以包含在 JDK 中。但是讨论集中在其他点上,没有人对此发表意见。我希望看到它也包括在个人身上。
  • 这太棒了。这个答案应该得到比 12 多得多的选票。好吧,现在是 13。
【解决方案2】:

我在这方面取得了一些进展。通过将锁变量显式声明为ReentrantReadWriteLock 而不是简单的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();
}

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

【讨论】:

  • 此外,我认为 getReadHoldCount 返回所有线程的计数(尽管文档没有说清楚)。虽然 RRWL 允许释放其他线程的读锁,但您的第二个 for 循环正在使用当前线程重新获取它们,这比您想要的要多。
  • 我认为您正在查看 getReadLOCKCount*(),而不是 getReadHOLDCount*()。后者在 Javadocs 中没有这样的警告,并且确实返回了当前线程的保持次数(例如,在线程 A 中锁定两次,然后在线程 B 中锁定 3 次,它将在线程 A 中返回 2 而不是 5)。
  • 没错!这几乎使我的两个 cmets 无效:-)
  • 很酷的解决方案。以后我会把它塞进我的包里。
  • 你可能会失去树的一致性。读取锁定是为了确保在使用它时不会发生任何变化。同时你试图用我们自己的双手改变这种结构。因此,当您继续迭代树时 - 它可能绝对是另一个,您将不得不返回到迭代的开头。
【解决方案3】:

你想做的应该是可能的。问题是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>

【讨论】:

    【解决方案4】:

    你想要做的事情根本不可能这样。

    您不能拥有可以毫无问题地从读升级为写的读/写锁。示例:

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

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

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

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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

        【解决方案6】:

        您正在寻找的是锁升级,使用标准 java.concurrent ReentrantReadWriteLock 是不可能的(至少不是原子的)。最好的方法是解锁/锁定,然后检查中间是否有人进行过修改。

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

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

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

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

        【讨论】:

        • 对,这就是问题所在。我并不是要强制所有读取锁,只是当前线程持有的那些。我很高兴写锁获取等待其他线程释放他们的读锁。
        • 事实上,JDK 实现在一定程度上会跟踪谁锁定了 readlock; getReadHoldCount() 仅返回当前线程的计数(通过 ThreadLocal),因此在这种情况下可以调用正确数量的 unlocks()。我在自己的答案中输入的代码确实有效。
        • 有趣,那么这在 jdk1.6 中已经改变了,jdk1.5 根本不使用线程局部变量。
        • 没有办法将读锁升级为保证成功的写锁。 “解锁/锁定,然后检查没有人在两者之间进行修改”的一种变体对我有用。
        【解决方案7】:

        这样的东西怎么样?

        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(),因为这两个方法都是同步的。跨度>
        【解决方案8】:

        到 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】:

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

          【讨论】:

          • 不容易。有一个访问者模式元素,这意味着我们锁定节点以供读取,然后调用其他类,这些类的逻辑可能会或可能不会回调指向同一个节点。此外(在更抽象的意义上)如果您不能以这种方式安全地使用它,为什么还要让锁重入呢? :-)
          • 这个想法是在客户端调用级别获取读锁。您的其他类可以指向“内部”方法(可能会受到保护),因为它们知道它们在读锁的上下文中运行。
          • 没错,我可以这样做,但是这些方法本身并不是线程安全的。如果客户不这样做,我尽可能不依赖需要获得具有微妙不正确(但通常显然是正确)行为的锁的客户端。
          【解决方案10】:

          那么,我们是否期望 java 仅在该线程尚未对 readHoldCount 做出贡献时才增加读取信号量计数?这意味着与仅仅维护 int 类型的 ThreadLocal readholdCount 不同,它应该维护 Integer 类型的 ThreadLocal Set(维护当前线程的 hasCode)。如果这没问题,我建议(至少现在)不要在同一个类中调用多个读取调用,而是使用一个标志来检查当前对象是否已经获得了读取锁。

          private volatile boolean alreadyLockedForReading = false;
          
          public void lockForReading(Lock readLock){
             if(!alreadyLockedForReading){
                lock.getReadLock().lock();
             }
          }
          

          【讨论】:

            【解决方案11】:

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

            【讨论】:

            • 正如我在问题中提到的,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() 的调用次数必须大于 1 - 但多少次?)。
            【解决方案12】:

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

            【讨论】:

            • 我不认为你理解我的问题,饥饿在这里不是问题。在一个线程中尝试这个: ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); lock.readLock().lock(); lock.readLock().lock(); lock.readLock().unlock(); lock.writeLock().lock();它仍然永远阻塞。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-12-04
            • 2011-01-25
            • 1970-01-01
            相关资源
            最近更新 更多