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

时间:2009-01-21 10:42:18

标签: java concurrency reentrantreadwritelock

我正在我的代码中使用ReentrantReadWriteLock来同步对树状结构的访问。这个结构很大,并且很多线程一次读取,偶尔会对它的一小部分进行修改 - 所以它似乎很适合读写习惯用法。我理解,对于这个特定的类,不能将读锁提升到写锁,因此根据Javadoc,必须在获得写锁之前释放读锁。我之前在非重入上下文中成功使用了这种模式。

然而,我发现我无法永久地阻止写入锁定。由于读锁是可重入的,我实际上正在使用它,简单的代码

lock.getReadLock().unlock();
lock.getWriteLock().lock()
如果我已经重新获得了readlock,

可以阻止。每次解锁调用只会减少保持计数,并且仅在保持计数达到零时才实际释放锁定。

编辑澄清这一点,因为我认为我最初没有解释得太清楚 - 我知道这个类中没有内置锁升级,而且我必须简单释放读锁并获取写锁。我的问题是,无论其他线程正在做什么,调用getReadLock().unlock()实际上可能不会释放线程对锁的保持,如果它重新获取它,在这种情况下调用{ <1}}将永远阻塞,因为此线程仍然保持读取锁定并因此阻塞自身。

例如,此代码段永远不会到达println语句,即使在没有其他线程访问锁的情况下运行单线程时也是如此:

getWriteLock().lock()

所以我问,有一个很好的习惯用法来处理这种情况吗?具体来说,当一个持有读锁定的线程(可能是重新发生)发现它需要进行一些写操作,因此想要“挂起”它自己的读锁定以获取写锁定(在其他线程上需要阻塞)释放他们在读锁定上的保留,然后“拿起”它在同一状态下对读锁的保持?

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

12 个答案:

答案 0 :(得分:27)

我在这方面取得了一些进展。通过将锁变量明确地声明为ReentrantReadWriteLock而不仅仅是ReadWriteLock(不太理想,但在这种情况下可能是必要的邪恶),我可以调用getReadHoldCount()方法。这让我可以获得当前线程的保持数,因此我可以多次释放readlock(之后重新获取相同的数字)。所以这是有效的,如快速而肮脏的测试所示:

final int holdCount = lock.getReadHoldCount();
for (int i = 0; i &lt; 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 &lt; holdCount; i++) {
      lock.readLock().lock();
   }
   lock.writeLock().unlock();
}

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

答案 1 :(得分: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>

答案 2 :(得分:21)

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

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

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

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

这种方法非常简单,正如我在并发兴趣中提到的那样,这个想法并不是全新的,因为至少在2000年之前就已经在Linux内核邮件列表中进行了讨论。同样是.Net平台的{ {3}}也支持锁升级。因此,直到现在,这个概念还没有在Java(AFAICT)上实现。

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

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

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

答案 3 :(得分:8)

你想做的事情就是不可能这样。

您不能拥有可以在没有问题的情况下从读取升级到写入的读/写锁定。例如:

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

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

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

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

答案 4 :(得分:4)

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

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

修改:
正如Ran Biron指出的那样,如果你的问题是饥饿(读锁定一直被释放,永远不会降到零),你可以尝试使用公平排队。但你的问题听起来不像是你的问题吗?

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

我实际上遇到了类似的问题,最后我写了自己的锁,跟踪谁有什么读锁并将这些锁升级为写锁。虽然这也是一种Write-on-Write类型的读/写锁(允许一位读者沿着读者),所以它仍然有点不同。

答案 5 :(得分:4)

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

http://www.javaspecialists.eu/archive/Issue215.html

的更多信息

答案 6 :(得分: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();
        }
    }
}

答案 7 :(得分: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);
  }
}

答案 8 :(得分:1)

到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 :(得分: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();
   }
}

答案 10 :(得分: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();
   }
 }

答案 11 :(得分:-1)

使用ReentrantReadWriteLock上的“fair”标志。 “公平”意味着锁定请求是先到先得的。您可能会遇到性能损失,因为当您发出“写入”请求时,所有后续“读取”请求都将被锁定,即使它们在预先存在的读锁仍然被锁定时可能已被服务。