HashMap对于不同的键是否线程安全?

105

如果我有两个多线程同时访问一个HashMap,但是可以保证它们永远不会同时访问同一个键,那么这样做还会导致竞争条件吗?

4个回答

124
在 @dotsid 的回答中,他说:

如果你以任何方式更改了一个 HashMap,那么你的代码就会破坏。

他是正确的。如果没有同步更新 HashMap,即使线程使用不相交的键集,也会出现问题。以下只是可能出现的问题之一:

  • 如果一个线程执行了 put,那么另一个线程可能会看到哈希映射大小的旧值。

  • 如果一个线程使用与第二个线程的键当前在同一个哈希桶中的键进行 put,则第二个线程的映射条目可能会丢失,暂时或永久地。这取决于哈希链(或其他结构)的实现方式。

  • 当一个线程执行触发表重建的 put 时,另一个线程可能会看到 hashtable 数组引用、它的大小、它的内容或哈希链的瞬态或旧版本。混乱可能会随之而来。

  • 当一个线程为与某些其他线程使用的键发生碰撞的键执行 put 时,并且后者线程为其键执行 put 时,则后者可能会看到哈希链引用的旧副本。混乱可能会随之而来。

  • 当一个线程使用与某些其他线程的键之一发生碰撞的键探测表时,它可能会在链上遇到该键。它将对该键调用 equals,并且如果线程不同步,则 equals 方法可能会在该键中遇到过时状态。

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

我可以想到三个解决方案:

  1. 使用 ConcurrentHashMap
  2. 使用常规的HashMap,但在外部进行同步,例如使用原始互斥锁、Lock对象等。但需注意,这可能会因锁争用导致并发瓶颈。
  3. 为每个线程使用不同的HashMap。如果线程确实具有不重叠的键集,则从算法的角度来看,它们无需共享单个Map。确实,如果您的算法涉及线程在某些时候迭代Map的键、值或条目,则将单个Map拆分成多个Map可能会大幅加速该处理部分。

1- 我们无法列举所有可能出现的问题。首先,我们无法预测所有JVM在各平台上如何处理JMM的未指定方面...。但您不应该依赖那种信息。您只需要知道像这样使用HashMap是基本错误的。即使您尚未观察到故障的症状,这样的应用程序也是有缺陷的。


你能详细说明一下混乱的类型吗?是无限循环吗?还是异常? - piepi
1
这两种情况都有可能发生,具体取决于HashMap的实现方式等因素。但是,请注意:无需枚举所有可能出错的情况。读者只需要知道任何这样做的代码都是不可靠的……因为它依赖于JLS或HashMap规范未保证的属性。 - Stephen C
@StephenC注意得非常好,但只是举了一个例子(其中有许多可能性),即从放置非null值的键中获取null值。线程根本没有共享密钥。即使在您的环境/单元测试等中工作,竞争条件问题=混乱*可能会发生。 - GriffoGoes

35

只需使用ConcurrentHashMap即可。ConcurrentHashMap使用多个锁来覆盖一系列哈希桶,以减少争用锁的机会。获取未争夺的锁会对性能产生微不足道的影响。

回答您最初的问题:根据javadoc的说法,只要映射的结构没有改变,您就可以放心使用。这意味着完全不删除元素,也不添加不在映射中的新键。替换与现有键相关联的值是可以的。

如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。(结构修改是添加或删除一个或多个映射的任何操作;仅更改实例已经包含的键的关联值不是结构修改。)

虽然它不保证可见性。因此,您必须愿意接受偶尔检索过时的关联。


6
这取决于你对"访问"的理解。如果你只是读取,即使是相同的键也可以读取,只要数据的可见性在“happens-before”规则下得到保证。这意味着HashMap不应更改,并且所有更改(初始构造)都应在任何读者开始访问HashMap之前完成。
如果以任何方式更改了HashMap,那么你的代码就会出现问题。@Stephen C提供了非常好的解释。
编辑:如果第一种情况是你的实际情况,我建议你使用Collections.unmodifiableMap()来确保你的HashMap永远不会被更改。由HashMap指向的对象也不应更改,因此积极使用final关键字可以帮助你。
正如@Lars Andren所说,ConcurrentHashMap在大多数情况下是最佳选择。

2
在我看来,ConcurrentHashMap是最好的选择。唯一没有推荐它的原因是作者没有询问 :)由于CAS操作,它的吞吐量较低,但正如并发编程的黄金法则所说:“先让它正确,然后再让它快” :) - Denis Bazhenov
unmodifiableMap 确保客户端不能修改该映射。但它无法确保底层映射不被更改。 - Pete Kirkham
正如我之前指出的那样:“被 HashMap 指向的对象也不应该改变”。 - Denis Bazhenov

5

在两个线程没有适当同步的情况下修改HashMap可能会很容易地导致竞态条件。

  • put()导致内部表的调整大小时,这需要一些时间,而另一个线程继续写入旧表。
  • 如果两个不同键的put()对于模表大小相等的哈希码导致相同桶的更新。(实际上,哈希码和桶索引之间的关系更加复杂,但仍然可能发生冲突。)

1
这比普通的竞态条件还糟糕。根据你所使用的HashMap实现的内部情况,可能会由于内存异常导致HashMap数据结构等出现损坏。 - Stephen C

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