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 方法可能会在该键中遇到陈旧状态。

如果您有两个线程同时执行 putremove 请求,则存在许多竞争条件的机会。

我能想到三个解决方案:

    使用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 对于不同的键是线程安全的吗?的主要内容,如果未能解决你的问题,请参考以下文章

HashMap是线程安全的吗?如何实现线程安全?

集合类关系

用过哪些Map,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如hashcode,扩容等

Java中HashMap,HashSet是线程安全的吗,ArrayList是线程不安全的那如何避免其出异常?

不在不同线程中重新评估昂贵的数据

HashMap在高并发下如果没有处理线程安全会有怎样的安全隐患,具体表现是什么