我需要编写某种特定的缓存实现,它具有唯一的键,但可以包含重复的值,例如:

 "/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不变,我至少可以避免不必要的数据复制。

这有意义吗?或者我可能在这里忽略了某些东西?谁能为此解决方案建议其他替代方案?

最佳答案

如果进行了此替换,则一次使用给定的Iterator仍将只有一个线程。
该警告表示两个线程不应使用相同的Iterator实例。并不是说两个线程不能同时进行迭代。

您可能会遇到的问题是,由于无法在ConcurrentMap的单个原子操作中完成删除操作,因此您可以让并发线程以中间状态查看映射:一个值已被删除,而另一个值未被删除。

我不确定这样做会更快,因为您说写性能不是问题,但是为了避免每次写操作都复制映射,您可以做的是使用ReadWriteLock来保护可变的ConcurrentMap。所有读取仍将是并发的,但是对映射的写入将阻止所有其他线程访问该映射。而且您不必每次修改都创建一个新副本。

10-05 20:41
查看更多