I need to write a somewhat specific implementation of a cache, which has unique keys but can contain duplicate values, e.g.:
"/path/to/one" -> 1
"/path/to/two" -> 2
"/path/to/vienas" -> 1
"/path/to/du" -> 2
The class needs to offer non blocking reads/key lookups but also has typical create/update/delete mutators. For example, removing value 2
should result
"/path/to/one" -> 1
"/path/to/vienas" -> 1
Reads for this cache will outweigh the writes by far so write performance is not an issue - as long as concurrent writes do not run on top of each other. The overall number of entries is much likely going to be less than 1000 so occasional iterating over values is still affordable.
So I wrote something like this (pseudo code):
//
// 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);
}
}
After writing the above, I realized that ConcurrentHashMap
also offers non-blocking reads which would make all my effort pointless, but there's a statement in its Javadoc which raises an eyebrow:
iterators are designed to be used by only one thread at a time
So if I replace the usage of volatile ImmutableMap
with final ConcurrentHashMap
and remove all synchronized
blocks, is it possible that competing concurrent mutators will invalidate each other? For example, I can imagine how two concurrent calls to remove
would lead to a race condition, completely invalidating the results of the first remove
.
The only improvement I can see is that by using final ConcurrentHashMap
and leaving synchronized
as they are I could at least avoid unnecessary copying of data.
Does that make sense - or maybe I'm overlooking something here? Can anyone suggest other alternatives for this solution?