ConcurrentHashMap 完全安全吗?

Posted

技术标签:

【中文标题】ConcurrentHashMap 完全安全吗?【英文标题】:Is ConcurrentHashMap totally safe? 【发布时间】:2013-02-03 13:23:15 【问题描述】:

这是一段来自 JavaDoc 关于ConcurrentHashMap 的文章。它说检索操作通常不会阻塞,因此可能与更新操作重叠。这是否意味着get() 方法不是线程安全的?

"然而,即使所有操作都是线程安全的,检索 操作不需要锁定,并且没有任何支持 以阻止所有访问的方式锁定整个表。这节课 在依赖其的程序中与 Hashtable 完全可互操作 线程安全,但不在其同步细节上。

检索操作(包括get)一般不会阻塞,所以可能 与更新操作重叠(包括放置和删除)。检索 反映最近完成的更新操作的结果 坚持他们的发病。”

【问题讨论】:

如果我没看错,这意味着检索将始终返回上次更新完成的结果——在检索开始的时间——。这意味着在检索开始和完成之间可能会发生完全更新,并且不会改变所述检索的结果。 【参考方案1】:

HashMaphashCode的基础上分为"buckets"。 ConcurrentHashMap 使用了这个事实。它的同步机制是基于阻塞桶而不是整个Map。这样,很少有线程可以同时写入几个不同的存储桶(一个线程一次可以写入一个存储桶)。

ConcurrentHashMap读取几乎不使用同步。当获取键值时使用同步,它看到null value。由于ConcurrentHashMap 不能将null 存储为值(是的,除了键,值也不能是nulls),这表明在读取时获取null 发生在初始化映射的中间另一个线程的条目(键值对):当键被分配,但值还没有,它仍然保持默认为空。 在这种情况下,读取线程需要等到条目被完全写入。

所以read() 的结果将基于地图的当前状态。如果您读取正在更新的键值,您可能会得到旧值,因为写入过程尚未完成。

【讨论】:

【参考方案2】:
ConcurrentHashMap 中的

get() 是线程安全的,因为它读取值 这是挥发性的。如果任何键的值为空,则 get() 方法等待直到它获得锁,然后它读取更新的 价值。

put() 方法正在更新 CHM 时,它会将该键的值设置为 null,然后它会创建一个新条目并更新 CHM。 get() 方法使用此空值作为另一个线程正在使用相同键更新 CHM 的信号。

【讨论】:

When put() method is updating CHM, then it sets the value of that key to null。请澄清一下!有链接吗?代码?【参考方案3】:

它是线程安全的。但是,线程安全的方式可能不是您所期望的。您可以从中看到一些“提示”:

这个类与Hashtable 在程序中完全可以互操作 依赖于它的线程安全而不是它的同步细节

要更全面地了解整个故事,您需要了解ConcurrentMap界面。

原来的Map 提供了一些非常基本的读取/更新方法。甚至我也能够实现Map 的线程安全;在很多情况下,如果不考虑我的同步机制,人们就无法使用我的 Map。这是一个典型的例子:

if (!threadSafeMap.containsKey(key)) 
   threadSafeMap.put(key, value);

这段代码不是线程安全的,尽管映射本身是线程安全的。同时调用containsKey() 的两个线程可能认为没有这样的密钥,因此它们都插入到Map 中。

为了解决这个问题,我们需要明确地进行额外的同步。假设我的 Map 的线程安全是通过同步关键字实现的,您需要这样做:

synchronized(threadSafeMap) 
    if (!threadSafeMap.containsKey(key)) 
       threadSafeMap.put(key, value);
    

这样的额外代码需要你了解地图的“同步细节”。在上面的例子中,我们需要知道同步是通过“synchronized”来实现的。

ConcurrentMap 接口更进一步。它定义了一些常见的“复杂”操作,涉及对地图的多次访问。例如,上面的例子暴露为putIfAbsent()。使用这些“复杂”操作,ConcurrentMap 的用户(在大多数情况下)不需要同步操作与对地图的多次访问。因此,Map 的实现可以执行更复杂的同步机制以获得更好的性能。 ConcurrentHashhMap 就是一个很好的例子。线程安全实际上是通过为映射的不同分区保持单独的锁来维护的。它是线程安全的,因为对 map 的并发访问不会破坏内部数据结构,或者导致任何更新丢失意外等。

了解以上所有,Javadoc的含义就会更加清晰:

“检索操作(包括 get)通常不会阻塞”,因为 ConcurrentHashMap 没有使用“同步”来保证其线程安全。 get 的逻辑本身负责线程安全;如果您进一步查看 Javadoc:

该表在内部进行了分区以尝试允许指定的数字 无争用的并发更新数

不仅检索是非阻塞的,甚至更新也可以同时发生。但是,非阻塞/并发更新并不意味着它是线程不安全的。它只是意味着它使用了一些不同于简单“同步”的方式来保证线程安全。

但是,由于内部同步机制没有暴露出来,如果你想做一些ConcurrentMap以外的复杂动作,你可能需要考虑改变你的逻辑,或者考虑不使用ConcurrentHashMap。例如:

// only remove if both key1 and key2 exists
if (map.containsKey(key1) && map.containsKey(key2)) 
    map.remove(key1);
    map.remove(key2);

【讨论】:

【参考方案4】:

get() 方法是线程安全的,其他用户为您提供了有关此特定问题的有用答案。

然而,尽管ConcurrentHashMapHashMap 的线程安全插入替代品,但重要的是要意识到,如果您正在执行多项操作,您可能必须大幅更改您的代码.比如下面这段代码:

if (!map.containsKey(key)) 
   return map.put(key, value);
else
   return map.get(key);

在多线程环境中,这是一种竞争条件。您必须使用ConcurrentHashMap.putIfAbsent(K key, V value) 并注意返回值,它会告诉您put 操作是否成功。阅读文档了解更多详情。


回答要求澄清为什么这是竞争条件的评论。

假设有两个线程AB 将分别在映射中放入两个不同的值v1v2,它们具有相同的键。密钥最初不存在于地图中。它们以这种方式交错:

线程A调用containsKey发现key不存在,立即挂起。 线程B调用containsKey发现key不存在,有时间插入其值v2。 线程A 恢复并插入v1,“和平地”覆盖(因为put 是线程安全的)线程B 插入的值。

现在线程B“认为”它已成功插入自己的值v2,但映射包含v1。这真的是一场灾难,因为线程 B 可能会调用 v2.updateSomething() 并且会“认为”地图的消费者(例如其他线程)可以访问该对象并且会看到可能重要的更新(“比如:这个访问者 IP address 正在尝试执行 DOS,从现在开始拒绝所有请求”)。相反,该对象将很快被垃圾收集并丢失。

【讨论】:

“不是 HashMap 的线程安全的替代品”。嗯,当然是。您是正确的,存在多个操作的竞争条件,但这并不能阻止 ConcurrentHashMap 成为线程安全的。 对于插入式替换,我的意思是:您将 HashMap 更改为 ConcurrentHashMap 并且地图上的所有常见操作(例如手工制作的“如果不存在则放置” - 我已经看过无数次了)自动线程安全。 同意。但是您的声明“ConcurrentHashMap 不是 HashMap 的线程安全替代品”至少具有极大的误导性。我会说这是不正确的。 我会修改它说“虽然 ConcurrentHashMap 是 HashMap 的线程安全替代品,但重要的是要意识到,如果你正在执行多个操作......” 为什么上面的代码 sn-ps 有竞态条件? ConcurrentHashmap.put() 不是线程安全的吗?我很困惑【参考方案5】:

这只是意味着当一个线程正在更新并且一个线程正在读取时,并不能保证第一个调用 ConcurrentHashMap 方法的线程及时将其操作先发生。

考虑一下关于告诉 Bob 在哪里的项目的更新。如果一个线程询问 Bob 在哪里,而另一个线程更新说他来到了“内部”,您无法预测阅读器线程会将 Bob 的状态设为“内部”还是“外部”。即使更新线程首先调用该方法,读取线程也可能获得“外部”状态。

线程之间不会互相引起问题。代码是线程安全的。

一个线程不会进入无限循环或开始生成奇怪的 NullPointerExceptions 或获得一半旧状态和一半新状态的“itside”。

【讨论】:

double-checked locking 模式能解决问题吗?通过强制正在寻找“内部”值的线程检查两次,写线程是否有机会更新该值?这样你会阻止写入而不是读取?【参考方案6】:

ConcurrentHashmap.get() 在某种意义上是线程安全的

不会抛出任何异常,包括ConcurrentModificationException 它将返回在过去某个(最近)时间为真的结果。这意味着两次对 get 的背靠背调用可以返回不同的结果。当然,其他任何Map 也是如此。

【讨论】:

“两个背靠背调用获取”是什么意思? 如果中间有 put() 或其他修改地图的操作,则背靠背调用 get 可能会返回不同的结果。 我不太同意您的回答,因为: 1. 不抛出任何异常并不意味着它是线程安全的,而 ConcurrentModificationException 与线程安全无关。 2.“两次背靠背调用相同的结果”绝不是Map的约定,它也与线程安全无关。 @MiserableVariable 但是即使他是新手,也并不意味着可以向他提供错误的答案(对不起,我看不到答案是“简化”,而只是不相关且不正确)跨度> 我认为这与值对象在 HashEntry 中的易失性有关。

以上是关于ConcurrentHashMap 完全安全吗?的主要内容,如果未能解决你的问题,请参考以下文章

迭代 ConcurrentHashMap 值线程安全吗?

ConcurrentHashMap完全解析(JDK6/7JDK8)

Java ConcurrentHashMap 在性能方面优于 HashMap 吗?

ConcurrentHashMap底层原理?

ConcurrentHashmap在我的实现中是否需要同步?

JAVA中线程安全的map都有哪些?