创建支持“快照”的ConcurrentHashMap

时间:2013-05-17 01:23:46

标签: java multithreading algorithm data-structures snapshot

我正在尝试创建一个支持“快照”的ConcurrentHashMap以提供一致的迭代器,并且想知道是否有更有效的方法来执行此操作。问题是如果同时创建两个迭代器,那么它们需要读取相同的值,并且并发哈希映射的弱一致迭代器的定义并不能保证这种情况。如果可能的话,我也想避免锁定:地图中有数千个值并且处理每个项目需要几十毫秒,而且我不希望在此期间阻止编写器,因为这可能导致编写器阻塞一分钟或更长时间。

到目前为止我所拥有的:

  1. ConcurrentHashMap's键是字符串,其值是ConcurrentSkipListMap<Long, T>
  2. 的实例
  3. 当使用putIfAbsent将元素添加到散列映射时,会分配新的跳转列表,并通过skipList.put(System.nanoTime(), t)添加对象。
  4. 要查询地图,我使用map.get(key).lastEntry().getValue()返回最新值。要查询快照(例如使用迭代器),我使用map.get(key).lowerEntry(iteratorTimestamp).getValue(),其中iteratorTimestamp是初始化迭代器时调用System.nanoTime()的结果。
  5. 如果删除了某个对象,我使用map.get(key).put(timestamp, SnapShotMap.DELETED),其中DELETED是一个静态最终对象。
  6. 问题:

    1. 是否有一个库已经实现了这个?或者除此之外,是否存在比ConcurrentHashMapConcurrentSkipListMap更合适的数据结构?我的密钥是可比较的,因此可能某种并发树比并发哈希表更好地支持快照。
    2. 如何防止这种情况持续增长?在完成X之前或之前初始化的所有迭代器之后,我可以删除键小于X的所有跳过列表条目(地图中的最后一个键除外),但我不知道确定何时的好方法这已经发生了:我可以标记迭代器在其hasNext方法返回false时已完成,但并非所有迭代器都必须运行完成;我可以将一个WeakReference保存到迭代器中,以便我可以检测它何时被垃圾收集,但除了使用遍历弱引用集合的线程之外,我无法想到检测此问题的好方法然后睡几分钟 - 理想情况下,线程会在WeakReference上阻塞,并在包装​​的引用为GC时通知,但我不认为这是一个选项。

      ConcurrentSkipListMap<Long, WeakReference<Iterator>> iteratorMap;
      while(true) {
          long latestGC = 0;
          for(Map.Entry<Long, WeakReference<Iterator>> entry : iteratorMap.entrySet()) {
              if(entry.getValue().get() == null) {
                  iteratorMap.remove(entry.getKey());
                  latestGC = entry.getKey();
              } else break;
          }
          // remove ConcurrentHashMap entries with timestamps less than `latestGC`
          Thread.sleep(300000); // five minutes
      }
      
    3. 编辑:为了澄清答案和评论中的一些混淆,我目前正在将弱一致的迭代器传递给公司另一个部门编写的代码,他们要求我增加力量迭代器的一致性。他们已经意识到,制作100%一致的迭代器对我来说是不可行的,他们只是想尽我所能。他们更关心吞吐量而不是迭代器的一致性,所以粗粒度锁不是一种选择。

3 个答案:

答案 0 :(得分:3)

您需要特殊实施的实际用例是什么?来自ConcurrentHashMap的Javadoc(强调补充):

  

检索反映了最近完成的更新操作的结果。 ... Iterators和Enumerations在迭代器/枚举的创建时或之后的某个时刻返回反映哈希表状态的元素。它们不会抛出ConcurrentModificationException。但是,迭代器被设计为一次只能由一个线程使用。

因此常规ConcurrentHashMap.values().iterator()将为您提供“一致”迭代器,但仅供单个线程一次性使用。如果您需要多次使用相同的“快照”和/或多个线程,我建议您复制地图。

编辑:通过新信息和坚持“强一致”迭代器,我提供了这个解决方案。请注意,使用ReadWriteLock具有以下含义:

  • 写入将被序列化(一次只有一个写入者),因此写入性能可能会受到影响。
  • 只要没有正在进行的写入,
  • 允许并发读取,因此读取性能影响应该是最小的。
  • Active 阅读器会阻止编写器,但只能检索对当前“快照”的引用。一旦线程拥有快照,无论处理快照中的信息需要多长时间,它都不再阻止编写器。
  • 任何写入处于活动状态时,
  • 读取器被阻止;一旦写完成,那么所有读者都可以访问新快照,直到新写入替换它为止。

通过序列化写入并在每次写入上对当前值进行复制来实现一致性。持有对“陈旧”快照的引用的读者可以继续使用旧快照而无需担心修改,垃圾收集器将在没有人再使用它时立即回收旧快照。假设读者有无要求从较早的时间点请求快照。

由于快照可能在多个并发线程之间共享,因此快照是只读的,无法修改。此限制也适用于从快照创建的任何remove()实例的Iterator方法。

import java.util.*;
import java.util.concurrent.locks.*;

public class StackOverflow16600019 <K, V> {
    private final ReadWriteLock locks = new ReentrantReadWriteLock();
    private final HashMap<K,V> map = new HashMap<>();
    private Collection<V> valueSnapshot = Collections.emptyList();

    public V put(K key, V value) {
        locks.writeLock().lock();
        try {
            V oldValue = map.put(key, value);
            updateSnapshot();
            return oldValue;
        } finally {
            locks.writeLock().unlock();
        }
    }

    public V remove(K key) {
        locks.writeLock().lock();
        try {
            V removed = map.remove(key);
            updateSnapshot();
            return removed;
        } finally {
            locks.writeLock().unlock();
        }
    }

    public Collection<V> values() {
        locks.readLock().lock();
        try {
            return valueSnapshot; // read-only!
        } finally {
            locks.readLock().unlock();
        }
    }

    /** Callers MUST hold the WRITE LOCK. */
    private void updateSnapshot() {
        valueSnapshot = Collections.unmodifiableCollection(
            new ArrayList<V>(map.values())); // copy
    }
}

答案 1 :(得分:2)

我发现ctrie是理想的解决方案 - 它是一个并发哈希数组映射trie,具有恒定时间快照

答案 2 :(得分:0)

解决方案1)如何只是同步put和迭代。这应该给你一致的快照。

解决方案2)开始迭代并使用布尔值来表示,然后覆盖puts,putAll以便它们进入队列,当迭代完成时,只需使用更改的值进行这些放置。