性能:ConcurrentHashMap与HashMap的比较

85

ConcurrentHashMap相比于HashMap在性能上如何表现,特别是.get()操作(我对只有少量项目的情况特别感兴趣,例如0-5000的范围内)?

是否有任何理由不使用ConcurrentHashMap而使用HashMap?

(我知道不允许使用null值)

更新

仅仅为了澄清,显然,在实际并发访问的情况下性能会受到影响,但在没有并发访问的情况下性能如何比较?

8个回答

113
我很惊讶地发现这个话题如此古老,但是还没有人提供任何关于此案例的测试。使用ScalaMeter,我创建了HashMapConcurrentHashMapaddgetremove测试,分别在以下两种情况下进行:

  1. 使用单个线程
  2. 使用尽可能多的可用核心数。 请注意,因为HashMap不是线程安全的,所以我为每个线程创建了一个单独的HashMap,但使用了一个共享的ConcurrentHashMap

代码可以在我的repo上找到。

结果如下:

  • X轴(大小)表示写入映射的元素数量
  • Y轴(值)表示时间(毫秒)

Add method Get method Remove method

总结

  • 如果您想尽可能快地操作数据,请使用所有可用的线程。这似乎很明显,每个线程有1/n的完整工作要做。

  • 如果选择单个线程访问,请使用HashMap,它更快。对于add方法,效率甚至高达3倍。只有getConcurrentHashMap上更快,但并不多。

  • 在使用多个线程操作ConcurrentHashMap时,与为每个线程操作单独的HashMaps相比,效果类似。因此,无需将数据分区到不同的结构中。

总之,当您使用单个线程时,ConcurrentHashMap的性能较差,但添加更多线程来完成工作肯定会加速进程。

测试平台

AMD FX6100,16GB Ram
Xubuntu 16.04,Oracle JDK 8更新91,Scala 2.11.8


6
分析不错,但是Collections.synchronizedCollection()会在每次读/写时锁定对象,这与ConcurrentHashMap的工作方式不同。因此,我不会尝试从你的统计数据中推断ConcurrentHashMap与HashMap(问题所问)的性能差异。也许你可以创建另一个问题:“Collections.synchronizedCollection()与TreeMap”,并在那里发表你的答案? - Jan Żankowski
1
@Atais,有什么软件可以绘制这个图形? - Flory Li
你能描述一下图表中的4个数据集吗?我不确定你是否在拿苹果和橙子比较。如果每个线程有HashMap的副本,那么应该将其与每个线程的ConcurrentHashMap的副本进行比较(这有一点意义)。如果你正在比较在线程之间共享数据的机制,则需要一个单一的数据结构实例,并比较不同的锁定机制。否则,你就是在比较不能正常工作的软件(由多个线程修改的HashMap)与可以正常工作的软件(由多个线程修改的ConcurrentHashMap)的性能。 - JJ Roman
基准测试似乎有缺陷,为什么“get”图表显示它是O(n)?它应该是O(1),或者在绝对最坏的情况下(Java 8之后)是O(log(n))。 - Deanveloper

96

线程安全是一个复杂的问题。如果你想使一个对象线程安全,请有意识地这样做,并记录这个选择。如果你的类是线程安全的,当它简化了用户的使用时,使用你的类的人会感谢你,但如果曾经是线程安全的对象在未来版本中变得不再安全,他们会咒骂你。线程安全虽然非常好,但并不仅限于圣诞节!

现在回到你的问题:

ConcurrentHashMap(至少在Sun当前的实现中)通过将底层映射分成若干个单独的桶来工作。获取元素本身不需要任何锁定,但它确实使用原子/易失性操作,这意味着一个内存屏障(潜在的非常昂贵,并且干扰其他可能的优化)。

即使在单线程情况下JIT编译器可以消除所有原子操作的开销,还是需要决定查找哪个桶——尽管这是一个相对较快的计算,但仍然不可能消除。

至于选择使用哪种实现,选择可能很简单。

如果这是一个静态字段,则几乎肯定要使用ConcurrentHashMap,除非测试表明这会严重影响性能。你的类与该类的实例具有不同的线程安全期望。

如果这是一个局部变量,那么HashMap可能就足够了——除非你知道对象的引用可能泄漏到另一个线程中。通过编写Map接口,可以在以后轻松地更改它,如果发现问题。

如果这是一个实例字段,并且该类未设计为线程安全,请将其标记为不安全,并使用HashMap。
如果您知道此实例字段是导致类不安全的唯一原因,并且愿意接受承诺线程安全所暗示的限制,则使用ConcurrentHashMap,除非测试显示存在显着的性能影响。在这种情况下,您可以考虑通过使用不同的工厂方法来允许类的用户选择线程安全版本的对象。
无论哪种情况,都要将类标记为线程安全(或有条件地线程安全),这样使用您的类的人就知道他们可以在多个线程中使用对象,编辑您的类的人也知道他们必须在将来保持线程安全。

7
@Stu,一年多后我找到了这篇帖子,并发现Bill的答案非常有帮助。无论原帖作者是否足够感激以接受答案,我仍然感谢Bill花时间写下了它。@Bill,谢谢! - cottonBallPaws
@littleFluffyKitty:听到这个消息真是太好了。我的评论更多地是针对@Mauli的:P - Stu Thompson
@BillMichell 谢谢!我刚刚发现了这个,你的回答不仅让决定变得非常清晰,而且教会了软件开发中一个重要的方面——永远不要忘记其他人将来可能会使用和编辑你的代码! - anuragw
3
有趣的是,"叔叔鲍勃"的《代码整洁之道》一书提到了这一点:ConcurrentHashMap 的实现在几乎所有情况下都比 HashMap 更快。没有提供支持此说法的数据,但我很想看到这个结果得到验证... - Dylan Watson
我接受了另一个答案,因为它更符合我的要求,尽管我想看到一些实际数字的性能比较。 - Mauli
@Bill Michell,你提到原子操作会减慢速度,这可能是正确的,但另外,拥有分离的桶可能会使CPU缓存难以包含高度使用的Map中的所有值,这可能会对性能产生影响,特别是在专门测试Map性能的任何测试中。 - mjaggard

4

我建议你进行测量,因为(其中一个原因是)可能会与你存储的特定对象的哈希分布有关。


3

标准哈希表没有并发保护,而并发哈希表有。在并发哈希表出现之前,您可以将哈希表包装起来以获得线程安全的访问,但这是粗略的粒度锁定,意味着所有并发访问都会串行化,这可能会影响性能。

并发哈希表使用锁分离,仅锁定受特定锁定影响的项。如果您正在运行像HotSpot这样的现代VM,则VM将尝试使用锁定偏好、锁合并和消除(lock biasing, coarsaning and ellision)等优化技术,因此您只在需要时才付出锁定的代价。

总之,如果您的哈希表将被并发线程访问,并且您需要保证其状态的一致性视图,请使用并发哈希表。


问题现在已经澄清,针对无并发访问的情况。 - Thilo

2
在使用10个锁的1000元素哈希表中,当有10000个线程插入和删除时,可以节省近一半的时间。详见这里
除了在分条(下面提到)成为频繁操作的情况下,始终使用并发数据结构。在这种情况下,您将不得不获取所有锁定吗?我读到最好的方法是通过递归来完成。
当有一种方法可以将高争用锁拆分为多个锁而不会影响数据完整性时,锁分条非常有用。是否可能需要一些思考,并且并非总是如此。数据结构也是做出决策的因素。因此,如果我们使用大型数组来实现哈希表,则使用单个锁定整个哈希表以进行同步将导致线程顺序访问数据结构。如果这是哈希表上的相同位置,则是必要的,但是,如果它们正在访问表的两个极端呢?
锁分条的缺点是很难获得受分条影响的数据结构的状态。在示例中,表的大小或尝试列出/枚举整个表可能很麻烦,因为我们需要获取所有分条锁定。

0
当然,一个没有任何锁定系统的Map会击败一个需要更多工作的线程安全行为的Map。 Concurrent Map的重点是在不使用synchronized的情况下保持线程安全,以比HashTable更快。 同样的图形对于ConcurrentHashMap与Hashtable(它是同步的)也非常有趣。

0
你在这里期望得到什么答案?
显然,这将取决于同时发生的读写操作的数量以及在写操作中正常地“锁定”一个映射需要多长时间(以及您是否会使用ConcurrentMap上的putIfAbsent方法)。任何基准测试都将在很大程度上毫无意义。

问题现在已经澄清,针对无并发访问的情况。 - Thilo

0

不清楚你的意思。如果你需要线程安全,几乎没有选择 - 只有ConcurrentHashMap。但是在get()调用中,它肯定会有性能/内存惩罚 - 访问易失性变量并锁定(如果你运气不好)。


1
我认为当前的实现从未在get()上锁定,但它们肯定会访问易失性变量。 - Bill Michell
这是的,看看源代码就知道了 :) - Vitaly

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