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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/464784/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 14:58:21  来源:igfitidea点击:

Java ReentrantReadWriteLocks - how to safely acquire write lock?

javaconcurrencyreentrantreadwritelock

提问by Andrzej Doyle

I am using in my code at the moment a ReentrantReadWriteLockto synchronize access over a tree-like structure. This structure is large, and read by many threads at once with occasional modifications to small parts of it - so it seems to fit the read-write idiom well. 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. I've used this pattern successfully in non-reentrant contexts before.

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

What I'm finding however is that I cannot reliably acquire the write lock without blocking forever. Since the read lock is reentrant and I am actually using it as such, the simple code

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

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

can block if I have acquired the readlock reentrantly. Each call to unlock just reduces the hold count, and the lock is only actually released when the hold count hits zero.

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

EDITto clarify this, as I don't think I explained it too well initially - I am aware that there is no built-in lock escalation in this class, and that I have to simply release the read lock and obtain the write lock. My problem is/was that regardless of what other threads are doing, calling getReadLock().unlock()may not actually release thisthread's hold on the lock if it acquired it reentrantly, in which case the call to getWriteLock().lock()will block forever as this thread still has a hold on the read lock and thus blocks itself.

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

For example, this code snippet will never reach the println statement, even when run singlethreaded with no other threads accessing the 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");

So I ask, is there a nice idiom to handle this situation? Specifically, when a thread that holds a read lock (possibly reentrantly) discovers that it needs to do some writing, and thus wants to "suspend" its own read lock in order to pick up the write lock (blocking as required on other threads to release their holds on the read lock), and then "pick up" its hold on the read lock in the same state afterwards?

所以我问,有没有一个很好的成语来处理这种情况?具体来说,当一个持有读锁(可能是可重入的)的线程发现它需要做一些写操作,因此想要“挂起”自己的读锁以获取写锁(在其他线程上按要求阻塞)释放他们对读锁的持有),然后在相同的状态下“拿起”它对读锁的持有?

Since this ReadWriteLock implementation was specifically designed to be reentrant, surely there is some sensible way to elevate a read lock to a write lock when the locks may be acquired reentrantly? This is the critical part that means the naive approach does not work.

由于这个 ReadWriteLock 实现是专门设计为可重入的,当可以重入获取锁时,肯定有一些明智的方法可以将读锁提升为写锁吗?这是关键部分,这意味着幼稚的方法不起作用。

采纳答案by Andrzej Doyle

I have made a little progress on this. By declaring the lock variable explicitly as a ReentrantReadWriteLockinstead of simply a ReadWriteLock(less than ideal, but probably a necessary evil in this case) I can call the getReadHoldCount()method. This lets me obtain the number of holds for the current thread, and thus I can release the readlock this many times (and reacquire it the same number afterwards). So this works, as shown by a quick-and-dirty test:

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

Still, is this going to be the best I can do? It doesn't feel very elegant, and I'm still hoping that there's a way to handle this in a less "manual" fashion.

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

回答by Ran Biron

Use the "fair" flag on the ReentrantReadWriteLock. "fair" means that lock requests are served on first come, first served. You could experience performance depredation since when you'll issue a "write" request, all of the subsequent "read" requests will be locked, even if they could have been served while the pre-existing read locks are still locked.

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

回答by falstro

What you're looking for is a lock upgrade, and is not possible (at least not atomically) using the standard java.concurrent ReentrantReadWriteLock. Your best shot is unlock/lock, and then check that noone made modifications inbetween.

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

What you're attempting to do, forcing all read locks out of the way is not a very good idea. Read locks are there for a reason, that you shouldn't write. :)

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

EDIT:
As Ran Biron pointed out, if your problem is starvation (read locks are being set and released all the time, never dropping to zero) you could try using fair queueing. But your question didn't sound like this was your problem?

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

EDIT 2:
I now see your problem, you've actually acquired multiple read-locks on the stack, and you'd like to convert them to a write-lock (upgrade). This is in fact impossible with the JDK-implementation, as it doesn't keep track of the owners of the read-lock. There could be others holding read-locks that you wouldn't see, and it has no idea how many of the read-locks belong to your thread, not to mention your current call-stack (i.e. your loop is killing allread locks, not just your own, so your write lock won't wait for any concurrent readers to finish, and you'll end up with a mess on your hands)

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

I've actually had a similar problem, and I ended up writing my own lock keeping track of who's got what read-locks and upgrading these to write-locks. Although this was also a Copy-on-Write kind of read/write lock (allowing one writer along the readers), so it was a little different still.

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

回答by Olivier

I suppose the ReentrantLockis motivated by a recursive traversal of the tree:

我想这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
}

Can't you refactor your code to move the lock handling outside of the recursion ?

你不能重构你的代码以将锁处理移到递归之外吗?

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

回答by xss

to OP: just unlock as many times as you have entered the lock, simple as that:

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

in the write lock as any self-respect concurrent program check the state needed.

在写锁中作为任何自尊并发程序检查所需的状态。

HoldCount shouldn't be used beyond debug/monitor/fast-fail detect.

不应在调试/监控/快速失败检测之外使用 HoldCount。

回答by Steffen Heil

What you are trying to do is simply not possible this way.

您尝试做的事情根本不可能以这种方式进行。

You cannot have a read/write lock that you can upgrade from read to write without problems. Example:

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

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

Now suppose, two threads would enter that function. (And you are assuming concurrency, right? Otherwise you would not care about locks in the first place....)

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

Assume both threads would start at the same timeand run equally fast. That would mean, both would acquire a read lock, which is perfectly legal. However, then both would eventually try to acquire the write lock, which NONE of them will ever get: The respective other threads hold a read lock!

假设两个线程将开始在同一时间和运行一样快。这意味着,两者都会获得一个读锁,这是完全合法的。但是,然后两者最终都会尝试获取写锁,而它们中的任何一个都不会获得:相应的其他线程持有读锁!

Locks that allow upgrading of read locks to write locks are prone to deadlocks by definition. Sorry, but you need to modify your approach.

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

回答by Bob

What you want to do ought to be possible. The problem is that Java does not provide an implementation that can upgrade read locks to write locks. Specifically, the javadoc ReentrantReadWriteLock says it does not allow an upgrade from read lock to write lock.

你想做的事情应该是可能的。问题是Java没有提供可以将读锁升级为写锁的实现。具体来说,javadoc ReentrantReadWriteLock 说它不允许从读锁升级到写锁。

In any case, Jakob Jenkov describes how to implement it. See http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgradefor details.

无论如何,Jakob Jenkov 描述了如何实现它。有关详细信息,请参阅http://tutorials.jenkov.com/java-concurrency/read-write-locks.html#upgrade

Why Upgrading Read to Write Locks Is Needed

为什么需要将读锁升级为写锁

An upgrade from read to write lock is valid (despite the assertions to the contrary in other answers). A deadlock can occur, and so part of the implementation is code to recognize deadlocks and break them by throwing an exception in a thread to break the deadlock. That means that as part of your transaction, you must handle the DeadlockException, e.g., by doing the work over again. A typical pattern is:

从读锁升级到写锁是有效的(尽管其他答案中的断言相反)。可能会发生死锁,因此实现的一部分是识别死锁并通过在线程中抛出异常来打破死锁来打破死锁的代码。这意味着作为事务的一部分,您必须处理 DeadlockException,例如,通过重新进行工作。一个典型的模式是:

boolean repeat;
do {
  repeat = false;
  try {
   readSomeStuff();
   writeSomeStuff();
   maybeReadSomeMoreStuff();
  } catch (DeadlockException) {
   repeat = true;
  }
} while (repeat);

Without this ability, the only way to implement a serializable transaction that reads a bunch of data consistently and then writes something based on what was read is to anticipate that writing will be necessary before you begin, and therefore obtain WRITE locks on all data that are read before writing what needs to be written. This is a KLUDGE that Oracle uses (SELECT FOR UPDATE ...). Furthermore, it actually reduces concurrency because nobody else can read or write any of the data while the transaction is running!

如果没有这种能力,实现一个可序列化的事务,一致地读取一堆数据,然后根据读取的内容写入一些东西的唯一方法是在开始之前预测写入是必要的,因此获得对所有数据的 WRITE 锁在写需要写的东西之前先读。这是 Oracle 使用的 KLUDGE (SELECT FOR UPDATE ...)。此外,它实际上降低了并发性,因为在事务运行时没有其他人可以读取或写入任何数据!

In particular, releasing the read lock before obtaining the write lock will produce inconsistent results. Consider:

特别是在获得写锁之前释放读锁会产生不一致的结果。考虑:

int x = someMethod();
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

You have to know whether someMethod(), or any method it calls, creates a reentrant read lock on y! Suppose you know it does. Then if you release the read lock first:

您必须知道 someMethod() 或它调用的任何方法是否在 y 上创建了可重入读锁!假设你知道它确实如此。然后如果你先释放读锁:

int x = someMethod();
y.readLock().unlock();
// problem here!
y.writeLock().lock();
y.setValue(x);
y.writeLock().unlock();

another thread may change y after you release its read lock, and before you obtain the write lock on it. So y's value will not be equal to x.

另一个线程可能会在你释放它的读锁之后和你获得它的写锁之前改变 y。所以 y 的值将不等于 x。

Test Code: Upgrading a read lock to a write lock blocks:

测试代码:将读锁升级为写锁块:

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

}

Output using Java 1.6:

使用 Java 1.6 输出:

read to write test
<blocks indefinitely>

回答by Vishal Angrish

So, Are we expecting java to increment read semaphore count only if this thread has not yet contributed to the readHoldCount? Which means unlike just maintaining a ThreadLocal readholdCount of type int, It should maintain ThreadLocal Set of type Integer (maintaining the hasCode of current thread). If this is fine, I would suggest (at-least for now) not to call multiple read calls within the same class, but instead use a flag to check, whether read lock is already obtained by current object or not.

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

private volatile boolean alreadyLockedForReading = false;

public void lockForReading(Lock readLock){
   if(!alreadyLockedForReading){
      lock.getReadLock().lock();
   }
}

回答by npgall

This is an old question, but here's both a solution to the problem, and some background information.

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

As others have pointed out, a classic readers-writer lock(like the JDK ReentrantReadWriteLock) inherently does not support upgrading a read lock to a write lock, because doing so is susceptible to deadlock.

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

If you need to safely acquire a write lock without first releasing a read lock, there is a however a better alternative: take a look at a read-write-updatelock instead.

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

I've written a ReentrantReadWrite_Update_Lock, and released it as open source under an Apache 2.0 license here. I also posted details of the approach to the JSR166 concurrency-interest mailing list, and the approach survived some back and forth scrutiny by members on that list.

我写了一个ReentrantReadWrite_Update_Lock,这下Apache 2.0许可发布为开放源代码在这里。我还在 JSR166 concurrency-interest 邮件列表 中发布了该方法的详细信息,并且该方法在该列表中成员的反复中幸免于难。

The approach is pretty simple, and as I mentioned on concurrency-interest, the idea is not entirely new as it was discussed on the Linux kernel mailing list at least as far back as the year 2000. Also the .Net platform's ReaderWriterLockSlimsupports lock upgrade also. So effectively this concept had simply not been implemented on Java (AFAICT) until now.

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

The idea is to provide an updatelock in addition to the readlock and the writelock. An update lock is an intermediate type of lock between a read lock and a write lock. Like the write lock, only one thread can acquire an update lock at a time. But like a read lock, it allows read access to the thread which holds it, and concurrently to other threads which hold regular read locks. The key feature is that the update lock can be upgraded from its read-only status, to a write lock, and this is not susceptible to deadlock because only one thread can hold an update lock and be in a position to upgrade at a time.

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

This supports lock upgrade, and furthermore it is more efficient than a conventional readers-writer lock in applications with read-before-writeaccess patterns, because it blocks reading threads for shorter periods of time.

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

Example usage is provided on the site. The library has 100% test coverage and is in Maven central.

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

回答by phi

Found in the documentation for ReentrantReadWriteLock. It clearly says, that reader threads will never succeed when trying to acquire a write lock. What you try to achieve is simply not supported. You mustrelease the read lock before acquisition of the write lock. A downgrade is still possible.

ReentrantReadWriteLock的文档中找到。它清楚地表明,当尝试获取写锁时,读取器线程永远不会成功。根本不支持您尝试实现的目标。您必须在获取写锁之前释放读锁。降级还是有可能的。

Reentrancy

This lock allows both readers and writers to reacquire read or write locks in the style of a {@link ReentrantLock}. Non-reentrant readers are not allowed until all write locks held by the writing thread have been released.

Additionally, a writer can acquire the read lock, but not vice-versa. Among other applications, reentrancy can be useful when write locks are held during calls or callbacks to methods that perform reads under read locks. If a reader tries to acquire the write lock it will never succeed.

重入性

此锁允许读取器和写入器以 {@link ReentrantLock} 的样式重新获取读或写锁。在写线程持有的所有写锁都被释放之前,不允许非可重入读者。

此外,作者可以获取读锁,但反之则不行。在其他应用程序中,当在调用或回调在读锁下执行读取的方法期间持有写锁时,可重入可能很有用。如果读者试图获取写锁,它将永远不会成功。

Sample usage from the above source:

来自上述来源的示例用法:

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