【问题标题】:Multithreaded usage of `ConcurrentHashMap` iterators`ConcurrentHashMap` 迭代器的多线程使用
【发布时间】:2012-01-19 22:35:39
【问题描述】:

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

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

这有意义吗 - 或者我在这里忽略了一些东西?任何人都可以为此解决方案提出其他替代方案吗?

【问题讨论】:

    标签: java caching concurrency guava concurrenthashmap


    【解决方案1】:

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

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

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

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-04-15
        • 2016-01-16
        • 1970-01-01
        • 1970-01-01
        • 2013-10-12
        • 2020-08-31
        • 2020-08-22
        • 1970-01-01
        相关资源
        最近更新 更多