可重入读写锁 - 多个读者同时,一个写者一次?

12

我对多线程环境有些陌生,正在尝试为以下情况找到最佳解决方案:

我每天早上从数据库中读取数据,并将数据存储在 Singleton 对象的 HashMap 中。当 intra-day 数据库更改发生时(每天可能会发生0-2次),只有调用 setter 方法。

还有一个 getter 方法,用于返回 map 中的元素,这个方法每天会被调用数百次。

我担心 getter 在我清空和重新创建 HashMap 时被调用,从而尝试在一个空/畸形列表中查找元素。如果我使这些方法同步化,它将阻止两个 reader 同时访问 getter,这可能会成为性能瓶颈。因为写入非常不频繁,所以我不想付出太大的性能损失。如果我使用 ReentrantReadWriteLock,它会强制要求任何调用 getter 的人排队等待写锁释放吗?它是否允许多个 reader 同时访问 getter?它是否强制只有一个 writer 可以同时写入?

编写这个代码只是...

private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private final Lock read = readWriteLock.readLock();
private final Lock write = readWriteLock.writeLock();

public HashMap getter(String a) {
    read.lock();
    try {
        return myStuff_.get(a);            
    } finally {
        read.unlock();
    }
}

public void setter() 
{
    write.lock();
    try {
        myStuff_ = // my logic
     } finally {
          write.unlock();
    }
}
3个回答

16

另一种实现这个目的的方法(不使用锁)是使用“写时复制”模式。它在你不经常进行写操作时效果很好。其思想是复制并替换字段本身。代码可能如下所示:

private volatile Map<String,HashMap> myStuff_ = new HashMap<String,HashMap>();

public HashMap getter(String a) {
    return myStuff_.get(a);
}

public synchronized void setter() {
    // create a copy from the original
    Map<String,HashMap> copy = new HashMap<String,HashMap>(myStuff_);
    // populate the copy
    // replace copy with the original
    myStuff_ = copy;
}

这样一来,读者是完全并发的,它们所付出的唯一代价就是对myStuff_进行了一个易变读取(这很少)。写者被同步以确保互斥。


优秀的回答。请注意,如果您知道只有一个编写者,您不需要同步“setter()”。此外,如果访问旧映射不是问题,则不需要使用“volatile”,因为对引用的访问始终是原子性的(但您可能会长时间使用旧映射)。 - ninjalj
2
如果只有一个写入者,同步可能会被跳过。但是你仍然需要使用volatile,因为它建立了happens-before关系。如果没有volatile,你将遭受重新排序的问题。 - sjlee
这就是我说“如果访问旧地图不是问题”的原因,而且“你可能会长时间使用旧地图” :) - ninjalj
6
拥有过期引用是一种后果,但重新排序可能会导致更严重的问题。由于重新排序,另一个线程可能会在setter操作完成之前看到新的引用(myStuff_)。 - sjlee
啊,是的,我错过了那个。所以volatile确实是必需的。 - ninjalj
只有当原子操作完成后,才能看到原子操作的任何副作用。对于所有声明为volatile的变量,读取和写入都是原子性的。因此,使用volatile可以使myStuff_ = copy成为原子操作,并且在执行getter(String a)时,如果myStuff_ = copy正在执行,则getter(String a)将被阻塞,直到myStuff_ = copy完成。 - 王奕然

2

0

你在一天开始的时候就开始了这个事情... 你每天会更新0-2次,而且每天要读它上百次。假设每次阅读需要1秒钟(非常长的时间),在8小时的工作日里(28800秒),你的读取负载仍然非常低。通过查看ReentrantReadWriteLock的文档,你可以“调整”模式,使其变得“公平”,这意味着等待时间最长的线程将获得锁。所以如果你将其设置为公平,我认为你的写入线程不会被饿死。

参考资料

ReentrantReadWriteLock


我所包含的代码是否实现了我所描述的多个读取器方案?我只想防止读取器在写入器正在进行时进行读取。每天有数百次读取,但在任何时候,它们可能会成批地出现并对我的getter发起20-30个调用。 - Sarah

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