【问题标题】:Creating a ConcurrentHashMap that supports "snapshots"创建一个支持“快照”的 ConcurrentHashMap
【发布时间】:2013-05-12 02:08:25
【问题描述】:

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

到目前为止我所拥有的:

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

问题:

  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
    }
    

编辑:为了澄清答案和 cmets 中的一些混淆,我目前正在将弱一致的迭代器传递给公司另一个部门编写的代码,他们要求我增加强度迭代器的一致性。他们已经意识到我做 100% 一致的迭代器是不可行的,他们只希望我尽最大努力。他们更关心吞吐量而不是迭代器的一致性,因此粗粒度锁不是一种选择。

【问题讨论】:

    标签: java multithreading algorithm data-structures snapshot


    【解决方案1】:

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

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

    所以常规的ConcurrentHashMap.values().iterator() 会给你一个“一致的”迭代器,但只供单个线程一次性使用。如果您需要多次和/或通过多个线程使用相同的“快照”,我建议制作地图副本。

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

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

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

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

    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
        }
    }
    

    【讨论】:

    • 我需要迭代器来反映地图的状态迭代器创建的时间,而不是在或自迭代器的时间创建。同样来自Javadoc:“视图的迭代器是一个“弱一致”迭代器,它......遍历元素,因为它们在构造迭代器时存在,并且可能(但不保证)反映构造后的任何修改;我需要一个不反映在创建迭代器后对地图所做的任何更改的迭代器
    • 在许多情况下,高并发线程中的执行时间使得所描述的“弱”一致性足够好满足大多数需要曾经在某个时间点(即使是现在)理解到,在任何点,该快照都可能已过时。但是,如果您真的需要如此强的一致性,则需要更多信息。 [编辑:我认为不需要任何 WeakReferences] 如果并发读取比写入频繁得多,一种选择是包装映射并通过 ReentrantReadWriteLock 进行控制。
    • +1 在并发用例中,必须清楚究竟是什么使映射的状态不正确在一微秒后,在这种情况下,在创建迭代器之后。您可以使用 ReradWriteLock 并在迭代之前获取 readlock。或者使用支持快照的 Copy-On-Write 集合。
    • 来自已编辑的问题:问题是如果同时创建了两个迭代器,那么它们需要读取相同的值...假设是两个迭代器是同时创建的。没有一个线程可以同时创建两个迭代器。当您有多个线程时,这在技术上是可行的,但是您怎么知道这两个线程试图“完全”同时创建一个迭代器?即使两个线程在完全相同的微秒内请求它,OS/JVM 也可以安排第三个线程在第一个迭代器之后但在第二个迭代器之前修改数据。
    • Zim-Zam,你把所有的要求都留给自己,然后用它们来拒绝提供给你的帮助。在多次请求之后,您还没有告诉我们系统的特性,以及现在,您想到的任何“清理”要求。我们不能给您任何“更好”的东西,因为没有人知道如何衡量“更好”,除非您向我们传达所有假设、要求和限制。祝你好运,但我无法读懂你的想法。
    【解决方案2】:

    解决方案 1) 仅在 put 和迭代上进行同步怎么样?这应该会给你一个一致的快照。

    Solution2) 开始迭代并生成一个布尔值,然后覆盖 puts、putAll 以便它们进入队列,当迭代完成时,只需使用更改后的值创建这些 puts。

    【讨论】:

    • 这在非并发环境中会很好,但在这种情况下,锁会杀死地图的吞吐量——我宁愿使用额外的内存并保持高度的并发性,而不是最小化内存并解决单线程性能
    【解决方案3】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-04-27
      • 1970-01-01
      • 1970-01-01
      • 2016-10-13
      • 2023-04-05
      • 1970-01-01
      • 2020-03-01
      相关资源
      最近更新 更多