HashMap 对于不同的键是线程安全的吗?
Posted
技术标签:
【中文标题】HashMap 对于不同的键是线程安全的吗?【英文标题】:Is a HashMap thread-safe for different keys? 【发布时间】:2011-02-10 22:20:16 【问题描述】:如果我有两个多线程访问一个 HashMap,但保证它们永远不会同时访问同一个键,那还会导致竞争条件吗?
【问题讨论】:
【参考方案1】:在@dotsid 的回答中,他这样说:
如果您以任何方式更改
HashMap
,那么您的代码就会被破坏。
他是对的。 HashMap
在没有同步的情况下更新,如果线程使用不相交的密钥集,even 也会中断。以下是只是一些1可能出错的地方。
如果一个线程执行put
,那么另一个线程可能会看到哈希图大小的陈旧值。
当一个线程执行put
触发表的重建时,另一个线程可能会看到哈希表数组引用、其大小、其内容或哈希链的临时或陈旧版本。可能会出现混乱。
当一个线程执行put
的键与某个其他线程使用的某个键发生冲突,而后一个线程对其键执行put
时,后者可能会看到哈希链引用的陈旧副本.可能会出现混乱。
当一个线程使用与其他线程的键之一冲突的键来探测表时,它可能会在链上遇到该键。它将在该键上调用 equals,如果线程未同步,则 equals 方法可能会在该键中遇到陈旧状态。
如果您有两个线程同时执行 put
或 remove
请求,则存在许多竞争条件的机会。
我能想到三个解决方案:
-
使用
ConcurrentHashMap
。
使用常规的HashMap
,但在外部同步;例如使用原始互斥体、Lock
对象等。
为每个线程使用不同的HashMap
。如果线程确实有一组不相交的键,那么(从算法的角度来看)它们应该不需要共享一个 Map。实际上,如果您的算法涉及线程在某个时间点迭代映射的键、值或条目,则将单个映射拆分为多个映射可以显着加快该部分的处理速度。
1 - 我们无法列举所有可能出错的事情。首先,我们无法预测所有 JVM 将如何在所有平台上处理 JMM 的 unspecified 方面。但是无论如何,您都不应该依赖此类信息。您需要知道的是,像这样使用HashMap
是根本错误的。执行此操作的应用程序已损坏...即使您尚未观察到症状...。
【讨论】:
你能详细说明混乱的类型吗?无限循环?例外? 其中任何一个都是可能的,具体取决于 HashMap 实现等。 HOWEVER - 既不可能也没有必要枚举所有可能出错的事情。读者需要知道的是,任何这样做的代码都是不可靠的......因为它依赖于 JLS 或HashMap
规范不保证的属性。
@StephenC 指出得很好,但作为一个例子(在许多可能性中)是从您放置非空值的键中获取 null
值。根本不共享密钥的线程。即使它在您的环境/单元测试/等中有效,竞争条件问题 = 混乱可能随之而来【参考方案2】:
这取决于您在“访问”下的意思。如果您只是阅读,只要在“happens-before”规则下保证数据的可见性,您甚至可以阅读相同的键。这意味着HashMap
不应更改,所有更改(初始构造)都应在任何读者开始访问HashMap
之前完成。
如果您以任何方式更改HashMap
,那么您的代码就会被破坏。 @Stephen C 提供了很好的解释。
编辑:如果第一种情况是您的实际情况,我建议您使用Collections.unmodifiableMap()
确保您的 HashMap 永远不会改变。 HashMap
指向的对象也不应该改变,所以积极地使用final
关键字可以帮助你。
正如@Lars Andren 所说,ConcurrentHashMap
在大多数情况下是最佳选择。
【讨论】:
ConcurrentHashMap 在我看来是一个最佳选择。我不推荐它的唯一原因,因为作者没有问它:) 由于 CAS 操作,它的吞吐量较低,但正如并发编程的黄金法则所说:“做对了,然后才让它快" :)unmodifiableMap
确保客户端无法更改地图。它不能确保底层地图没有改变。
正如我已经指出的:“HashMap 指向的对象也不应该改变”【参考方案3】:
在两个线程没有正确同步的情况下修改 HashMap 很容易导致竞争条件。
当put()
导致调整内部表的大小时,这需要一些时间,并且其他线程会继续写入旧表。
如果键的哈希码与表大小的模数相等,则不同键的两个put()
会导致更新同一个桶。 (其实hashcode和bucket index的关系比较复杂,但还是有可能发生冲突的。)
【讨论】:
这比竞争条件更糟糕。根据您使用的HashMap
实现的内部结构,您可能会因内存异常而损坏HashMap
数据结构等。【参考方案4】:
只需使用 ConcurrentHashMap。 ConcurrentHashMap 使用多个锁来覆盖一系列哈希桶,以减少锁被争用的机会。获取无竞争锁对性能的影响很小。
回答你原来的问题:根据javadoc,只要地图的结构没有改变,你就可以了。这意味着根本不会删除元素,也不会添加地图中尚未存在的新键。替换与现有键关联的值很好。
如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改映射,则必须在外部同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)
虽然它不能保证可见性。所以你必须愿意接受偶尔检索过时的关联。
【讨论】:
以上是关于HashMap 对于不同的键是线程安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章
用过哪些Map,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如hashcode,扩容等