3

我需要在我的应用程序中实现一个计数器映射(如 Map)。然而,这个结构应该被多个线程访问。

看起来ConcurrentHashMap<Key, Long>不是一个合适的解决方案,对吧?

ConcurrentHashMap<Key, AtomicLong>我反而想了想。

但是有一个问题 - 增量请求分布不均。很少有最流行的 Key 可以有高达 95% 的对该数据结构的所有增量请求。

据我了解,这将导致对单个AtomicLong实例的并发访问,并且应该发生许多锁,这会在一定程度上降低效率。

问题 1:有没有更好的解决方案 - 也许是更好的数据类型而不是AtomicLong,它允许增量的短累积或类似的东西?

问题 2:我想定期(也许每分钟)将结构持久化到磁盘,并且我想保持其“实际”状态(所有最近的更新都已解决?) - 最直接的方法是什么?

4

2 回答 2

4

首先,您在这里面临“过早优化”的风险。您担心的并发热点/瓶颈很有可能并不重要。

话说回来:

除非并发热点是一个ConcurrentHashMap<Key, AtomicLong>主要问题,否则这听起来是一个不错的选择。应该在ConcurrentHashMap很大程度上避免映射的并发问题,并且AtomicLong除非在单个计数器上存在极端争用,否则将提供良好的性能。

有没有更好的解决方案——也许是更好的数据类型而不是 AtomicLong,它允许增量的短累积或类似的东西?

那可能行得通。(例如,每个线程可以有自己的(非并发)映射,并使用Long或非同步的自定义long持有者类,而不是AtomicLong。)

但是,这样做的缺点是:

  • 内存使用量可能会大得多。
  • 您将有额外的开销将多个地图合并为一个以获得最终计数。该步骤很可能涉及串行计算。

总而言之,除非您拥有大量内核和非常高的计数率,否则如果这提高了性能,我会感到惊讶。

我想定期(也许每分钟)将结构持久化到磁盘,并且我想保持其“实际”状态(所有最近的更新都已解决?) - 最直接的方法是什么?

最直接的方法是在坚持的同时停止一切。

如果这是不可接受的,那么您需要执行以下操作:

  1. 决定坚持。
  2. 创建一个新ConcurrentHashMap<Key, AtomicLong>实例。
  3. 原子地用新的地图替换现有的地图......这样你就可以累积计数。
  4. 坚持旧地图。
  5. 迭代旧映射,原子地将每个键的计数转移到新映射。
  6. 丢弃旧地图。
于 2013-08-10T05:57:34.170 回答
4

是什么让您认为 AtomicLong 在内部使用锁?这不是真的,它主要建立在CAS操作之上。我的建议是使用 AtomicLong 来实现它,然后再分析实现。如果(且仅当)您的计数器将成为瓶颈,则考虑将其替换为任何其他实现。

“我们应该忘记小的效率,比如大约 97% 的时间:过早的优化是万恶之源” - Donald Knuth

对于状态持久性,最简单的方法是序列化您的地图:

ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(map);
objOut.close();
于 2013-08-10T05:44:24.750 回答