我需要编写一个特定的缓存实现,它具有唯一的密钥,但可以包含重复的值,例如:
"/path/to/one" -> 1
"/path/to/two" -> 2
"/path/to/vienas" -> 1
"/path/to/du" -> 2
该类需要提供非阻塞读取/密钥查找,但也有典型的创建/更新/删除更改器。例如,删除值2
应该
"/path/to/one" -> 1
"/path/to/vienas" -> 1
此缓存的读取将超过写入,因此写入性能不是问题 - 只要并发写入不会在彼此之上运行。条目的总数很可能小于1000,因此偶尔迭代值仍然是可以承受的。
所以我写了这样的东西(伪代码):
//
// tl;dr all writes are synchronized on a single lock and each
// resets the reference to the volatile immutable map after finishing
//
class CopyOnWriteCache {
private volatile Map<K, V> readOnlyMap = ImmutableMap.of();
private final Object writeLock = new Object();
public void add(CacheEntry entry) {
synchronized (writeLock) {
readOnlyMap = new ImmutableMap.Builder<K, V>()
.addAll(readOnlyMap)
.add(entry.key, entry.value)
.build();
}
}
public void remove(CacheEntry entry) {
synchronized (writeLock) {
Map<K, V> filtered = Maps.filterValues(readOnlyMap, somePredicate(entry));
readOnlyMap = ImmutableMap.copyOf(filtered);
}
}
public void update(CacheEntry entry) {
synchronized (writeLock) {
Map<K, V> filtered = Maps.filterValues(readOnlyMap, somePredicate(entry));
readOnlyMap = new ImmutableMap.Builder<K, V>()
.addAll(filtered)
.add(entry.key, entry.value)
.build();
}
}
public SomeValue lookup(K key) {
return readOnlyMap.get(key);
}
}
在写完上述内容之后,我意识到ConcurrentHashMap
还提供非阻塞读取,这会使我的所有努力都毫无意义,但是它的Javadoc中有一个声明引起了人们的注意:
iterators are designed to be used by only one thread at a time
因此,如果我将volatile ImmutableMap
的用法替换为final ConcurrentHashMap
并删除所有synchronized
块,那么竞争的并发更改器是否可能互相失效?例如,我可以想象两个对remove
的并发调用如何导致竞争条件,完全使第一个remove
的结果无效。
我能看到的唯一改进是,通过使用final ConcurrentHashMap
和离开synchronized
,我至少可以避免不必要的数据复制。
这是否有意义 - 或者我可能会忽视这里的某些事情?任何人都可以为此解决方案提出其他替代方案吗?
答案 0 :(得分:5)
如果你做了这个替换,你仍然只有一个线程一次使用给定的迭代器。 警告意味着两个线程不应使用相同的Iterator实例。不是两个线程不能同时迭代。
您可能遇到的问题是,由于删除操作无法在ConcurrentMap的单个原子操作中完成,您可以让并发线程看到处于中间状态的映射:一个值已被删除但不是另一个。
我不确定这会更快,因为你说写性能不是问题,但是你可以做的就是在每次写入时避免映射的副本,就是使用一个守护可变ConcurrentMap的ReadWriteLock 。所有读取仍然是并发的,但是对映射的写入将阻止所有其他线程访问映射。而且每次修改时都不必创建新的副本。
答案 1 :(得分:2)
可以同时通过多个迭代器/多个线程来变异ConcurrentHashMap
。只是你不应该将迭代器的单个实例交给多个线程并同时使用它。
因此,如果您使用ConcurrentHashMap
,则无需在此处留下synchronized
。正如JB Nizet所指出的,这与您当前实现的区别在于中间状态的可见性。如果您不关心这一点,使用ConcurrentHashMap
将是我的首选,因为实现最简单,您不必担心读写性能。