我正在尝试实现一个读/写缓冲区类,它可以支持多个编写器和读取器,读者可以在编写缓冲区时同时读取缓冲区。这是我的代码,到目前为止我还没有看到任何问题,但我不能100%确定这是否是线程安全的,或者是否有更好的方法。
public class Buffer{
private StringBuilder sb = new StringBuilder();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private Random random = new Random();
public void read(){
try{
lock.readLock().lock();
System.out.println(sb.toString());
} finally{
lock.readLock().unlock();
}
}
public void write(){
try{
lock.writeLock().lock();
sb.append((char)(random.nextInt(26)+'a'));
} finally{
lock.writeLock().unlock();
}
}
}
答案 0 :(得分:9)
多线程安全无任何问题!读写锁可以保护对StringBuilder的访问,代码简洁易读。
通过使用ReentrantReadWriteLock,您实际上最大化了实现更高并发度的机会,因为多个读者可以一起进行,因此这比使用普通的旧同步方法更好。但是,与问题中所述相反,代码确实不允许作者在读者阅读时写作。但这本身并不一定是个问题。
读者在继续操作之前获取读锁定。在继续之前,编写器获取写锁定。读锁的规则允许在没有写锁时获取一个(但如果有一些读锁,即如果有更多活动读取器则可以)。当且仅当没有其他锁(没有读者,没有编写者)时,写锁的规则允许获取一个。因此允许多个读者,但只允许一个作者。
可能需要的唯一更改是将锁定初始化代码更改为:
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
因为问题中给出的原始代码不要求锁是公平的。通过上述更改,可以保证“线程使用近似到达顺序策略争用入口。当释放写入锁定时,最长等待的单个写入器将被分配写入锁定,或者如果读取器等待的时间长于任何作家,读者都将获得读锁定。当构造为非公平时,进入锁定的顺序不必是到达顺序。“ (摘自http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html)
另请参阅以下内容(来自同一来源):
ReentrantReadWriteLocks可用于在某些类型的集合的某些用途中提高并发性。这通常是值得的,只有当预期集合很大时,由更多的读取器线程访问而不是编写器线程,并且需要具有超过同步开销的开销的操作。例如,这是一个使用TreeMap的类,该类预计很大并且可以同时访问。
class RWDictionary {
private final Map<String, Data> m = new TreeMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock(); try { return m.get(key); } finally { r.unlock(); }
}
public String[] allKeys() {
r.lock(); try { return m.keySet().toArray(); } finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock(); try { return m.put(key, value); } finally { w.unlock(); }
}
public void clear() {
w.lock(); try { m.clear(); } finally { w.unlock(); }
}
}
API文档的摘录特别注重性能。在您的具体情况下,我无法评论您是否符合“大集合”标准,但我可以说输出到控制台比线程安全机制开销更耗时。无论如何,从逻辑的角度来看,使用ReentrantReadWriteLocks是完全合理的,并且完全是线程安全的。这是一个很好的代码: - )
注1(回答有关原始问题评论中发现的例外情况的问题): 取自http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/Lock.html lock()获取锁。 如果锁定不可用,则当前线程将被禁用以进行线程调度,并且在获取锁定之前处于休眠状态。
Lock实现可能能够检测到锁的错误使用,例如可能导致死锁的调用,并且可能在这种情况下抛出(未经检查的)异常。必须通过Lock实现记录环境和异常类型。
ReentrantReadWriteLock.ReadLock(http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.ReadLock.html)或ReentrantReadWriteLock.WriteLock(http://docs.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.WriteLock.html)
的相关文档中未说明此类异常注意2:虽然对LockBuilder的访问受到锁的保护,但System.out却没有。特别是,多个读取器可以同时读取该值并尝试同时输出它。这也没关系,因为对System.out.println()的访问是同步的。
注意3:如果要禁止多个活动写入程序,但允许写入程序和一个或多个读取程序同时处于活动状态,则可以简单地完全跳过使用读取锁定,即删除lock.readLock()。lock( );和lock.readLock()。unlock();在你的代码中。但是,在这种特殊情况下,这是错误的。您需要停止并发读取和写入StringBuilder。
答案 1 :(得分:-1)
描述和代码似乎是两个不同的东西。你在描述中说,你想让读者在作者(我一次假设一个人)写的时候读。但是,您也可以锁定读取方法。所以现在你有一个,读者或作者一次访问你的缓冲区。
如果您希望在有作家的情况下让读者访问,请从读取方法中删除锁定。