`ConcurrentHashMap`迭代器的多线程用法

时间:2012-01-19 22:35:39

标签: java caching concurrency guava concurrenthashmap

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

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

这是否有意义 - 或者我可能会忽视这里的某些事情?任何人都可以为此解决方案提出其他替代方案吗?

2 个答案:

答案 0 :(得分:5)

如果你做了这个替换,你仍然只有一个线程一次使用给定的迭代器。 警告意味着两个线程不应使用相同的Iterator实例。不是两个线程不能同时迭代。

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

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

答案 1 :(得分:2)

可以同时通过多个迭代器/多个线程来变异ConcurrentHashMap。只是你不应该将迭代器的单个实例交给多个线程并同时使用它。

因此,如果您使用ConcurrentHashMap,则无需在此处留下synchronized。正如JB Nizet所指出的,这与您当前实现的区别在于中间状态的可见性。如果您不关心这一点,使用ConcurrentHashMap将是我的首选,因为实现最简单,您不必担心读写性能。