与 HashMap 相比,ConcurrentHashMap 的性能如何,尤其是 .get() 操作(我对只有少数项目的情况特别感兴趣,可能在 0-5000 之间)?
有什么理由不使用 ConcurrentHashMap 而不是 HashMap?
(我知道不允许使用空值)
更新
只是为了澄清一下,显然实际并发访问的情况下的性能会受到影响,但是如何比较没有并发访问的情况下的性能呢?
与 HashMap 相比,ConcurrentHashMap 的性能如何,尤其是 .get() 操作(我对只有少数项目的情况特别感兴趣,可能在 0-5000 之间)?
有什么理由不使用 ConcurrentHashMap 而不是 HashMap?
(我知道不允许使用空值)
更新
只是为了澄清一下,显然实际并发访问的情况下的性能会受到影响,但是如何比较没有并发访问的情况下的性能呢?
我真的很惊讶地发现这个话题如此古老,但还没有人提供任何关于这个案例的测试。使用ScalaMeter
我创建了add
,get
和remove
两种情况HashMap
下的测试ConcurrentHashMap
:
HashMap
不是线程安全的,所以我只是HashMap
为每个线程单独创建了一个,但使用了一个 shared ConcurrentHashMap
。代码可在我的 repo中找到。
如果您想尽快对数据进行操作,请使用所有可用线程。这似乎很明显,每个线程都有 1/n 的全部工作要做。
如果选择单线程访问使用HashMap
,那简直是更快。对于add
方法,它的效率甚至提高了 3 倍。只有get
在 上更快ConcurrentHashMap
,但不多。
当对多个线程进行操作时,对每个线程ConcurrentHashMap
单独进行操作同样有效。HashMaps
因此,无需将数据划分为不同的结构。
总而言之,ConcurrentHashMap
当你使用单线程时性能更差,但是添加更多线程来完成工作肯定会加快进程。
测试平台
AMD FX6100、16GB Ram
Xubuntu 16.04、Oracle JDK 8 更新 91、Scala 2.11.8
线程安全是一个复杂的问题。如果你想让一个对象线程安全,有意识地去做,并记录那个选择。使用你的类的人会感谢你,如果它是线程安全的,因为它简化了他们的使用,但是如果一个曾经是线程安全的对象在未来的版本中变得不是这样,他们会诅咒你。线程安全虽然非常好,但不仅仅适用于圣诞节!
所以现在回答你的问题:
ConcurrentHashMap(至少在Sun 当前的实现中)通过将底层映射划分为多个单独的桶来工作。获取元素本身不需要任何锁定,但它确实使用原子/易失性操作,这意味着内存屏障(可能非常昂贵,并且会干扰其他可能的优化)。
即使在单线程情况下,JIT 编译器可以消除所有原子操作的开销,但仍然存在决定查看哪个桶的开销 - 诚然,这是一个相对快速的计算,但无论如何,它是不可能消除。
至于决定使用哪个实现,选择可能很简单。
如果这是一个静态字段,您几乎肯定要使用 ConcurrentHashMap,除非测试表明这是一个真正的性能杀手。您的类与该类的实例具有不同的线程安全期望。
如果这是一个局部变量,那么 HashMap 就足够了——除非您知道对该对象的引用可能会泄漏到另一个线程。通过对 Map 接口进行编码,您可以在以后发现问题时轻松更改它。
如果这是一个实例字段,并且该类尚未设计为线程安全的,则将其记录为非线程安全的,并使用 HashMap。
如果您知道该实例字段是该类不是线程安全的唯一原因,并且愿意忍受承诺线程安全所暗示的限制,那么请使用 ConcurrentHashMap,除非测试显示显着的性能影响。在这种情况下,您可能会考虑允许该类的用户以某种方式选择对象的线程安全版本,也许是通过使用不同的工厂方法。
在任何一种情况下,将类记录为线程安全(或有条件的线程安全),以便使用您的类的人知道他们可以跨多个线程使用对象,并且编辑您的类的人知道他们将来必须维护线程安全。
我建议您测量它,因为(出于一个原因)可能对您存储的特定对象的散列分布有一定的依赖性。
标准 hashmap 不提供并发保护,而并发 hashmap 提供。在它可用之前,您可以包装 hashmap 以获得线程安全访问,但这是粗粒度锁定,意味着所有并发访问都被序列化,这可能会真正影响性能。
并发 hashmap 使用锁剥离并且只锁定受特定锁影响的项目。如果您在现代虚拟机(例如热点)上运行,虚拟机将尽可能尝试使用锁定偏置、粗调和省略,因此您只需在实际需要时为锁定支付罚金。
总之,如果您的地图将被并发线程访问,并且您需要保证其状态的一致视图,请使用并发哈希图。
对于 1000 个元素的哈希表,对整个表使用 10 个锁可以节省近一半的时间,即插入 10000 个线程并从中删除 10000 个线程。
有趣的运行时间差异在这里
始终使用并发数据结构。除非条带化的缺点(如下所述)成为频繁操作。在那种情况下,你将不得不获得所有的锁?我读到最好的方法是递归。
当有一种方法可以在不影响数据完整性的情况下将高争用锁分解为多个锁时,锁条带化非常有用。如果这是可能的或不应该考虑一下,但并非总是如此。数据结构也是决定的因素。所以如果我们使用一个大数组来实现一个哈希表,对整个哈希表使用一个锁来同步它会导致线程顺序访问数据结构。如果这是哈希表上的同一位置,那么这是必要的,但是,如果他们正在访问表的两个极端怎么办。
锁条带化的缺点是很难获得受条带化影响的数据结构的状态。在示例中,表的大小或尝试列出/枚举整个表可能很麻烦,因为我们需要获取所有条带锁。
你在这里期待什么答案?
这显然取决于与写入同时发生的读取次数以及法线贴图必须在您的应用程序中的写入操作上“锁定”多长时间(以及您是否会使用putIfAbsent
on 方法ConcurrentMap
)。任何基准都将在很大程度上毫无意义。
不清楚你的意思。如果你需要线程安全,你几乎别无选择——只有 ConcurrentHashMap。并且在 get() 调用中肯定会产生性能/内存损失——如果你不走运,可以访问 volatile 变量并锁定。
当然,没有任何锁系统的 Map 会胜过需要更多工作的具有线程安全行为的 Map。Concurrent 的要点是在不使用同步的情况下是线程安全的,因此比 HashTable 更快。对于 ConcurrentHashMap 与 Hashtable(同步),相同的图形会非常有趣。