HashMap对于不同的密钥是线程安全的吗?


Answers:


99

在@dotsid的回答中,他说:

如果您以任何方式更改HashMap,则代码将被破坏。

他是正确的。即使线程使用的是不相交的键集,在没有同步的情况下更新的HashMap也会中断。这是一些可能出错的事情。

  • 如果一个线程执行put,则另一线程可能会看到哈希图大小的陈旧值。

  • 当线程执行put触发表重建的操作时,另一个线程可能会看到哈希表数组引用的瞬时或陈旧版本,其大小,其内容或哈希链。可能会发生混乱。

  • 当线程对put与其他线程使用的某个密钥冲突的密钥执行a操作,而后者put对密钥进行操作时,后者可能会看到哈希链引用的陈旧副本。可能会发生混乱。

  • 当一个线程用与其他某个线程的键之一冲突的键探测表时,它可能会在链上遇到该键。它将在该键上调用equals,如果线程未同步,则equals方法可能在该键上遇到陈旧状态。

并且,如果您有两个线程同时执行putremove请求操作,则有很多竞争条件的机会。

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

  1. 使用ConcurrentHashMap
  2. 使用常规HashMap但在外部同步;例如使用原始互斥量,Lock对象等。
  3. HashMap为每个线程使用不同的名称。如果线程确实有一组不相交的键,那么(从算法的角度来看)应该不需要它们共享单个Map。确实,如果您的算法涉及某个线程在某个点迭代映射的键,值或条目,则将单个映射拆分为多个映射可以显着加快该部分处理。

30

只需使用ConcurrentHashMap。ConcurrentHashMap使用覆盖多个哈希桶的多个锁,以减少争用锁的机会。获取无争议的锁会对性能产生边际影响。

回答您的原始问题:根据javadoc,只要地图的结构不变,就可以了。这意味着根本不会删除任何元素,也不会添加地图中尚未存在的新键。替换与现有键关联的值就可以了。

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

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


6

这取决于您在“访问”下的含义。如果您只是阅读,只要按“事前发生”规则保证了数据的可见性,您甚至可以阅读相同的键。这意味着HashMap不应更改,所有更改(初始构造)都应在任何读者开始访问之前完成HashMap

如果您HashMap以任何方式进行更改,那么您的代码就会被破坏。@Stephen C提供了很好的解释。

编辑:如果第一种情况是您的实际情况,我建议您使用Collections.unmodifiableMap()以确保您的HashMap永不更改。指向的对象HashMap也不应更改,因此积极使用final关键字可以为您提供帮助。

正如@Lars Andren所说,ConcurrentHashMap在大多数情况下是最佳选择。


2
我认为ConcurrentHashMap是最佳选择。我不推荐它的唯一原因是,因为作者没有问它:)由于CAS操作,它的吞吐量较低,但是正如并发编程的黄金法则所说:“做到正确,然后才能使其快速进行。 “ :)
Denis Bazhenov 2010年

unmodifiableMap确保客户端无法更改地图。它不会做任何事情来确保基础图不变。
皮特·柯卡姆

正如我已经指出的那样:“由HashMap指向的对象也不应更改”
Denis Bazhenov,2010年

4

在没有两个线程正确同步的情况下修改HashMap可能很容易导致竞争状态。

  • 当一个put()导致内部表大小的调整时,这会花费一些时间,而另一个线程将继续写入旧表。
  • put()如果键的哈希码对表大小取模,则两个不同的键会导致同一存储桶的更新。(实际上,哈希码和存储桶索引之间的关系更为复杂,但是仍然可能发生冲突。)

1
这比仅仅比赛条件还差。根据HashMap您使用的实现的内部情况,您可能HashMap会由于内存异常而导致数据结构损坏等。
Stephen C
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.