9

我正在尝试实现一个读/写缓冲区类,它可以支持多个写入器和读取器,并且读取器可以在写入器写入缓冲区时同时读取缓冲区。这是我的代码,到目前为止我还没有看到任何问题,但我不能 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();
            }
       }
} 
4

2 回答 2

11

多线程安全没有任何问题!读写锁保护对 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 实现可能能够检测到锁的错误使用,例如会导致死锁的调用,并且在这种情况下可能会抛出(未经检查的)异常。该锁实现必须记录情况和异常类型。

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:虽然对 StringBuilder 的访问受到锁的保护,但 System.out 不受。特别是,多个读取器可能会同时读取该值并尝试同时输出它。这也没关系,因为对 System.out.println() 的访问是同步的。

注意 3:如果你想禁止多个活跃的写入者,但允许一个写入者和一个或多个读取者同时活跃,你可以简单地跳过使用读锁,即删除 lock.readLock().lock(); 和 lock.readLock().unlock(); 在你的代码中。但是,在这种特殊情况下,这是错误的。您需要停止对 StringBuilder 的并发读写。

于 2012-11-27T16:06:14.267 回答
-1

描述和代码似乎是两个不同的东西。你在描述中说你想让读者阅读,而作者(我假设一次一个)写作。但是,您的读取方法也有锁定。因此,现在您一次只有一个读取器或写入器访问您的缓冲区。

如果您想在有写入器的情况下让读取器访问,请从读取方法中删除锁定。

于 2012-11-27T16:06:10.333 回答