【问题标题】:Ideal Java Data Structure for Streaming data流数据的理想 Java 数据结构
【发布时间】:2012-11-08 20:32:42
【问题描述】:

我有一个特定的用例,但无法确定要使用的正确数据结构。

我有一个线程将流对象保存到 HashMap 中。类似于市场数据,其中您有很高且未知的变动频率。

另一个线程不断读取此映射以获取更新的价格对象并按特定顺序按键查询。在给定的周期中,对于同一个键的查询可能会多次。读取和写入非常频繁,但读取线程只对完全更新的最新可用数据感兴趣,并且在写入完成之前不一定会阻塞。

我希望您对此类用例的理想数据结构有想法。有比 ConcurrentHashMap 更好的实现吗?

谢谢

【问题讨论】:

  • hashmap 是否会在更新分时数据时被修改(即是否会有放置和删除)?还是会在数据开始传入之前设置映射?
  • 是的,会有很多puts,但没有removes。基本上在每个到达的滴答声上,我都会做一个 put(key,Price)。另一方面,我也可以使用一些虚拟对象预先填充 hashMap,因为我事先知道键。

标签: java performance collections


【解决方案1】:

ConcurrentHashMap。来自 Javadoc

支持全并发检索和可调整的哈希表 更新的预期并发。此类遵循相同的功能 规范为 Hashtable,并包括方法的版本 对应Hashtable的各个方法。然而,即使所有 操作是线程安全的,检索操作不需要 锁定,并且不支持将整个表锁定在 一种阻止所有访问的方法。这个类是完全可互操作的 程序中的哈希表依赖于其线程安全但不依赖于其 同步详情。

检索操作(包括get)一般不会阻塞,所以可能 与更新操作重叠(包括放置和删除)。检索 反映最近完成的更新操作的结果 坚持他们的发病。对于 putAll 和 明确的,并发检索可能只反映插入或删除 一些条目。同样,迭代器和枚举返回元素 反映哈希表在某个时间点或之后的状态 创建迭代器/枚举。

【讨论】:

    【解决方案2】:

    一种方法是写时复制方案,如下所示:

    public class Prices {
        private volatile Map<String, Integer> prices = Collections.emptyMap();
    
        public void putPrice(String ticker, int price) {
            HashMap<String, Integer> newPrices = new HashMap<String, Integer>(prices);
            newPrices.put(ticker, price);
            prices = newPrices;
        }
    
        public Integer getPrice(String ticker) {
            return prices.get(ticker);
        }
    }
    

    这对获取的开销最小 - 从 volatile 读取,然后是正常的哈希查找。但是,它对 puts 有很大的开销——创建一个全新的映射,以及对 volatile 的写入。如果您的读写比率很高,这可能仍然是一个很好的权衡。

    您可以通过仅在实际需要添加新条目而不是更新现有条目时更改地图来改善这一点;您可以通过使用可变值来实现:

    public class Prices {
        private volatile Map<String, AtomicInteger> prices = Collections.emptyMap();
    
        public void putPrice(String ticker, int price) {
            AtomicInteger priceHolder = prices.get(ticker);
            if (priceHolder != null) {
                priceHolder.set(price);
            }
            else {
                HashMap<String, AtomicInteger> newPrices = new HashMap<String, AtomicInteger>(prices);
                newPrices.put(ticker, new AtomicInteger(price));
                prices = newPrices;
            }
        }
    
        public Integer getPrice(String ticker) {
            AtomicInteger priceHolder = prices.get(ticker);
            if (priceHolder != null) return priceHolder.get();
            else return null;
        }
    }
    

    我不确定AtomicInteger 的性能特点是什么;这可能比看起来要慢。假设AtomicInteger 的速度不是太慢,这应该非常快——它涉及从 volatile 读取两次加上每次获取的正常哈希查找,以及从 volatile 读取、哈希查找和对 volatile 的一次写入更新现有价格。它仍然涉及复制地图以添加新价格。然而,在典型的市场中,这种情况并不经常发生。

    【讨论】:

      【解决方案3】:

      如果在更新数据时未修改映射(即没有放置或删除),您甚至不需要像 ConcurrentHashMap 这样的同步映射。如果程序执行过程中不断有puts和removes,则需要同步这些调用。然而,当更新频率变高时(在多线程程序中),即使是 ConcurrentHashMap 也会开始抛出 ConcurrentModificationExceptions。什么频率太高?您可能需要自己衡量,这取决于您平台中的许多因素。

      在这些情况下,我尝试创建一种情况,在程序执行期间我不必在映射中插入或删除,仅在数据流停止时启动和关闭时。如果实在不行,我就用普通的HashMap和优秀的数据结构CopyOnWriteArrayList结合,对外同步。我没有测试过 ConcurrentHashMap 的限制,但我不会相信它用于我自己的生产系统。

      编辑:ConcurrentHashMap 不会导致任何 ConcurrentModificationExceptions,只有当您使用 Collections.synchronizedMap 时,您可能会遇到麻烦。

      【讨论】:

      • 能否请您解释一下ConcurrentHashMap在什么情况下会抛出什么样的异常以提高更新频率
      • 我在适当的地方也使用了 CopyOnWriteArrayList。我的问题是关于你的陈述“但是,当更新频率变高时,即使是 ConcurrentHashMap 也会开始抛出 ConcurrentModificationExceptions ”
      • 如果您特别考虑 ConcurrentModificationException,那么 ConcurrentMap 将永远不会抛出该异常。
      • 嗯,你让我到了那里,我看到的是 Collections.synchronizedMap 返回的地图,导致重负载下的异常。将添加更正
      • 这是一篇不错的文章,如果您还没有看过的话:ibm.com/developerworks/java/library/j-jtp07233/index.html
      猜你喜欢
      • 2019-08-24
      • 1970-01-01
      • 1970-01-01
      • 2011-11-23
      • 1970-01-01
      • 2014-11-20
      • 2011-05-19
      • 1970-01-01
      • 2011-01-31
      相关资源
      最近更新 更多