【问题标题】:Checking for readLock possession in `ReentrantReadWriteLock`检查 `ReentrantReadWriteLock` 中的 readLock 所有权
【发布时间】:2016-12-13 17:54:00
【问题描述】:

在尝试使用ReentrantReadWriteLock 控制对具有许多不同读写操作的相当复杂的数据结构的访问时,我遇到了一些密切相关的问题。根据文档中的示例,我已经获取了一个读取和写入锁,并且我正在使用它们(据我所知)成功管理对该数据结构的并发访问。然而,在调试另一个问题时,我注意到有时我在同一个线程中获得了多个读锁。其基本原因是我有许多复杂的查询会调用更简单的查询(请参见下面的示例)。复杂查询可以被认为是一个事务,即在getVersion()myQuery 中对data 的访问之间不应该有写入。这个当前的解决方案有效,但这意味着在代码中的某些地方,我将拥有同一个线程拥有的多个读锁。我注意到写等效项有一个isHeldByCurrentThread() 方法,但奇怪的是readLock 中没有。我找不到以下内容:

  • 在同一个线程中拥有多个读锁是不是很糟糕(风格不佳、性能不佳或存在未来错误的风险)?
  • 使用WriteLock.isHeldByCurrentThread 来检查错误是不好的(例如,期望写锁但不存在,或者作为释放写锁的条件)?
  • 如果这是一个问题,是否有最佳实践来决定如何进行?我是否将 lockedAquired 布尔值传递给可能从已经拥有锁的代码中调用的方法,我是否确保它们是唯一获取的,还是应该使用 ReadLock.tryLock()

这是代码示例:

public class GraphWorldModel {

  //unfair RW Lock (default, see javadoc)
  protected final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(false);
  protected final ReentrantReadWriteLock.ReadLock readLock = rwl.readLock();
  protected final ReentrantReadWriteLock.WriteLock writeLock = rwl.writeLock();

  protected long versionID = 0;
  protected Object data = null;

  @Override
  public long getVersion() {
      long result = -1;
      readLock.lock();
      try {
          result = this.versionID;
      } finally {

          readLock.unlock();
      }
      return result;
  }
  public ResultObject myQuery() {
      //do some work
      readLock.lock();
      try {
        long version = getVersion();
        ResultObject result = new ResultObject(this.data, version);
        //note: querying version and result should be atomic!
      } finally {
        readLock.unlock();
      }
      //do some more work
      return result;
  }
}

【问题讨论】:

  • 锁,顾名思义,是可重入的。因此,当您已经拥有它时再次获得它根本不是问题。你所拥有的一切都很好。

标签: java multithreading concurrency reentrantreadwritelock


【解决方案1】:
  • 在同一个线程中拥有多个读锁是不是很糟糕(风格不佳、性能不佳或存在未来错误的风险)?

我会说这是有问题的风格。首先,正如@JBNizet 指出的那样,您尝试再次获取您已经拥有的锁没有问题。锁只是将读取器计数增加一,然后在最终解锁时减少它。

但是,这确实意味着您必须跨越内存屏障(volatile 读取)来更新共享锁统计信息,这意味着性能下降。这取决于这段代码的执行频率是否会产生明显的性能差异。

然而,就错误而言,有一个很大的问题。如果您只是在谈论只读锁,那么我认为不会有更多的错误机会。实际上试图解决双锁问题(见下文)可能有更大的风险。但在您的代码中,try/finally 逻辑可确保正确使用锁并在完成时解锁。

然而,如果你在谈论混合读写锁,那么你需要了解以下代码死锁:

ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
ReadLock readLock = rrwl.readLock();
WriteLock writeLock = rrwl.writeLock();
readLock.lock();
writeLock.lock();

或者至少在其他代码解锁read-lock 之前它会停止。如果您要在代码中混合读写锁,则必须防止双重锁定。

  • 使用 WriteLock.isHeldByCurrentThread 检查错误(例如,期望写入锁但不存在,或者作为释放写入锁的条件)是不好的?

这将为您的代码添加大量逻辑,这会带来更大的风险,但如果性能影响很大,则可能值得。也就是说,请参阅下面的替代方案。

  • 如果这是一个问题,是否有最佳实践来决定如何进行?我是否将lockedAquired 布尔值传递给可能从已经拥有锁的代码中调用的方法,我是否确保它们是唯一获取的,还是应该使用ReadLock.tryLock()?

我要做的是创建一个名为getVersionLocked() 的内核方法。比如:

@Override
public long getVersion() {
    readLock.lock();
    try {
        // NOTE: I refactored this to return out of the try/finally which is a fine pattern
        return getVersionLocked();
    } finally {
        readLock.unlock();
    }
}
public ResultObject myQuery() {
    //do some work
    readLock.lock();
    ResultObject result;
    try {
      long version = getVersionLocked();
      result = new ResultObject(this.data, version);
      //note: querying version and result should be atomic!
    } finally {
      readLock.unlock();
    }
    //do some more work
    return result;
}
// NOTE: to call this method, we must already be locked
private long getVersionLocked() {
    return this.versionID;
}

然后您可以决定调用锁定或解锁版本的方法,并且只调用一次readLock()

这风险更大,因为当锁没有持有时您可能会调用getVersionLocked(),但我认为这是可以接受的风险。

【讨论】:

  • 没有“内存同步”。线程保持计数是按线程保持的。 java.util.concurrent 基本上即使不是完全同步也是免费的。
  • 我不认为你是正确的@EJP。在Sync 类中有一个volatile int statetryAcquireShared(...) 中被击中,所以当我读到它时,每次调用ReadLock.lock() 都有一个volatile 访问,尽管我同意保持计数是线程本地。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-16
  • 2021-08-08
  • 2018-04-16
相关资源
最近更新 更多