Java:并发递增计数器的映射

3

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

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

我考虑使用ConcurrentHashMap<Key, AtomicLong>代替。

但是有一个问题——增量请求不会均匀分布。少数最受欢迎的键可能会有高达95%的所有增量请求到达这个数据结构。

据我所知,这将导致对单个AtomicLong实例的并发访问,并且会产生许多锁,从而降低效率。

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

问题2:我想定期将结构持久化到磁盘上(也许是每分钟一次),我希望持久化它的“实际”状态(包括所有最近的更新已被解决?)-最简单的方法是什么?

2个回答

4

你为什么认为AtomicLong内部使用锁?这不是真的,它主要建立在CAS操作之上。我的建议是使用AtomicLong实现并稍后对其进行分析。如果(仅当)计数器成为瓶颈时,请考虑将其替换为其他任何实现。

"我们应该忘记小的效率问题,大约97%的时间:过早优化是万恶之源" - 唐纳德·科诺斯

至于状态持久性,最简单的方法是序列化您的映射:

ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream objOut = new ObjectOutputStream(out);
objOut.writeObject(map);
objOut.close();

感谢您指出AtomicLong的工作方式与我想象中不同的事实!关于持久化,主要问题不是技术编写映射,而是“是否需要采取措施确保所有最后更新都将通过简单序列化此数据结构进行持久化?”很抱歉我没有表达清楚。 - Rodion Gorkovenko
据我所知,不需要采取任何特定的措施。在进行序列化时,CHM逐个锁定段,因此所有最新的更新都应该包括在内。然而,序列化本身并不是原子性的。如果在序列化过程中进行了一些更新,则根据您的运气,它们可能会或可能不会包含在生成的快照中。如果这对您很重要,请考虑下面帖子中Stephen的建议来交换映射。 - Jk1

4
首先,您可能存在“过早优化”的风险。您担心的并发瓶颈可能并不重要。
话虽如此:
除非并发热点是一个主要问题,否则ConcurrentHashMap 听起来是一个不错的选择。 ConcurrentHashMap应该可以避免地图的并发问题,而AtomicLong会在单个计数器上出现极端争用时提供良好的性能。
“是否有更好的解决方案 - 也许是更好的数据类型而不是AtomicLong,它允许简短的增量累积或类似的东西?”
这可能有效。 (例如,每个线程都可以拥有自己的(非并发)映射,并使用Long或非同步的自定义long holder类,而不是AtomicLong。)
但是,这样做的缺点是:
内存使用可能显着增加。
您需要将多个映射组合成一个以获取最终计数的额外开销。该步骤很可能涉及串行计算。
总而言之,除非您有大量核心和非常高的计数速率,否则我会感到惊讶,如果这能提高性能。
“我想定期将结构保存到磁盘(也许每分钟一次),我希望保存其“实际”状态(所有最近的更新都已解决) - 最简单的方法是什么?”
最简单的方法是在持久化时“停止一切”。
如果不接受这种方法,那么您需要执行以下操作:
决定持久化。
创建一个新的ConcurrentHashMap 实例。
原子替换现有地图为新地图...以便您可以累积计数。
保存旧地图。
迭代旧地图,原子地将每个键的计数转移到新地图中。
丢弃旧地图。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接