在尝试使用ReentrantReadWriteLock
来控制对具有许多不同的读写操作的相当复杂的数据结构的访问时,出现了一些密切相关的问题。按照文档中的示例,我已经获取了读和写锁,并且正在使用它们(据我所知)成功管理对该数据结构的并发访问。但是,在调试另一个问题时,我注意到有时我在同一线程中获得了多个读锁。造成这种情况的基本原因是,我有许多调用简单查询的复杂查询(请参见下面的示例)。可以将复杂查询视为事务,即getVersion()
和data
中对myQuery
的访问之间不应存在写操作。当前的解决方案有效,但是这意味着在代码的某些位置,我将拥有同一线程拥有的多个读取锁。我注意到等效的写方法有一个isHeldByCurrentThread()
方法,但是readLock
却奇怪地没有这种方法。我找不到以下内容:
在同一个线程中拥有多个读取锁是否不好(风格差,性能低下或存在将来发生bug的风险)?
使用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;
}
}
最佳答案
在同一个线程中拥有多个读取锁是否不好(风格差,性能低下或存在将来发生bug的风险)?
我会说这是可疑的风格。首先,正如@JBNizet指出的那样,尝试重新获得已经拥有的锁没有问题。锁定只会将读取器的数量增加一,然后在最终解锁时将其减少。
但是,这确实意味着您必须越过内存障碍(volatile
读取)来更新共享锁统计信息,这意味着性能下降。这取决于该代码执行的频率是否会产生任何明显的性能差异。
在错误方面,有一个很大的陷阱。如果您仅在谈论只读锁,那么我认为没有更多的机会出错。实际上,尝试解决双重锁定问题(请参阅下文)可能会有更大的风险。但是在您的代码中,try / finally逻辑可确保正确使用锁并在完成后将其解锁。
但是,如果您正在谈论混合读写锁,那么您需要了解以下代码死锁:
ReentrantReadWriteLock rrwl = new ReentrantReadWriteLock();
ReadLock readLock = rrwl.readLock();
WriteLock writeLock = rrwl.writeLock();
readLock.lock();
writeLock.lock();
或者至少停止,直到其他代码将
read-lock
解锁。如果要在代码中混合使用读和写锁,则必须防止双重锁。使用WriteLock.isHeldByCurrentThread检查错误是不好的(例如,期望写锁定但不存在写锁定,或作为释放写锁定的条件)吗?
这将在代码中添加很多逻辑,这虽然风险较大,但如果性能影响较大,则可能值得这样做。也就是说,请参阅下面的替代方法。
是否有最佳实践来决定如果遇到问题如何进行?我是否应该将已锁定的布尔值传递给可能已经从具有锁的代码中调用的方法,是否要确保它们是唯一获得的,还是应该使用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()
,但是我认为这是可以接受的风险。