Java可重入读写锁 - 如何在读锁定时安全地获取写锁定?

75

目前我的代码中正在使用重入读写锁(ReentrantReadWriteLock)来同步访问类似树形结构的数据。这个结构很大,被许多线程同时读取,并且偶尔对其进行小部分修改 - 因此似乎很适合使用读写模式。我知道使用这个特定的类,无法将读锁升级为写锁,因此根据Javadocs,在获取写锁之前必须释放读锁。我以前在非可重入环境中成功地使用过这种模式。

然而我发现,我无法可靠地获取写锁而不会一直阻塞。由于读锁是可重入的,我实际上正在使用它作为这样的锁,简单的代码

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

如果我已经重入地获取了读锁,那么可能会出现阻塞。每次调用 unlock 只会减少 hold count,只有当 hold count 减为零时,锁才会被实际释放。

编辑 为了澄清这一点,因为我认为我最初没有解释得太好 - 我知道在这个类中没有内置的锁升级,我必须简单地释放读锁并获取写锁。我的问题是/曾经是,无论其他线程正在做什么,调用 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
12个回答

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

正如我在问题中提到的那样,“我知道对于这个特定的类,不能将读锁升级为写锁,因此根据Javadocs,在获取写锁之前必须释放读锁。” 我的关注点完全在于如何可靠地释放读锁,当它以可重入的方式被获取时(因此必须调用rwl.readLock().unlock()多次大于1次 - 但是具体是多少次呢?)。 - Andrzej Doyle

-1
在ReentrantReadWriteLock上使用“fair”标志。 “公平”意味着锁请求按先到先服务的顺序处理。您可能会遇到性能降低,因为当您发出“写入”请求时,所有后续的“读取”请求都将被锁定,即使它们在现有的读取锁仍被锁定时也可以提供服务。

1
我认为你没有理解我的问题,饥饿在这里不是问题。尝试在单个线程中执行以下操作: ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true); lock.readLock().lock(); lock.readLock().lock(); lock.readLock().unlock(); lock.writeLock().lock(); 它仍然会永久阻塞。 - Andrzej Doyle

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接