ConcurrentHashMap完全安全吗?

时间:2013-02-19 00:18:59

标签: java multithreading synchronized java.util.concurrent

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

  

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

     

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

6 个答案:

答案 0 :(得分:47)

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

但是,尽管ConcurrentHashMapHashMap的线程安全的 drop-in 替代品,但重要的是要意识到如果您正在进行多项操作,则可能需要显着改变您的代码。例如,请使用以下代码:

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并发现该密钥不存在,但会立即暂停。
  • 线程B调用containsKey并发现该密钥不存在,并且有时间插入其值v2
  • 线程A恢复并插入v1,“和平”覆盖(因为put是线程安全的)线程B插入的值。

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

答案 1 :(得分:16)

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

  

此类与程序中的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就是一个很好的例子。事实上,线程安全是通过为地图的不同分区保留单独的锁来维护的。它是线程安全的,因为对映射的并发访问不会破坏内部数据结构,或导致任何更新丢失意外等。

考虑到上述所有因素,Javadoc的含义将更加清晰:

“检索操作(包括get)通常不会阻止”因为ConcurrentHashMap没有使用“synchronized”来保证其线程安全性。 get本身的逻辑关注线程安全性;如果你在Javadoc中进一步观察:

  

该表在内部分区以尝试允许指定的数字   没有争用的并发更新

检索非阻塞,甚至更新都可以同时发生。但是,非阻塞/并发更新并不意味着它是线程UNsafe。它只是意味着除了简单的“同步”之外,它还使用了一些其他方法来保证线程安全。

但是,由于未公开内部同步机制,如果您想执行除ConcurrentMap提供的操作之外的一些复杂操作,您可能需要考虑更改逻辑,或者考虑不使用{{1} }。例如:

ConcurrentHashMap

答案 2 :(得分:9)

从某种意义上说,

ConcurrentHashmap.get()是线程安全的

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

答案 3 :(得分:7)

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

ConcurrentHashMap 读取几乎不使用同步。在获取密钥值时使用同步,它会看到null 。由于ConcurrentHashMap无法将null存储为值(是的,除了键,值也不能是null s),因此建议在阅读时提取null另一个线程初始化map entry (键值对)的中间:当指定了key,但是值还没有,并且它仍然保持 default null。
在这种情况下,读取线程需要等到输入完全写入。

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

答案 4 :(得分:5)

  ConcurrentHashMap中的

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

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

答案 5 :(得分:4)

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

考虑项目的更新,告诉鲍勃在哪里。如果一个线程询问Bob所处的位置,而另一个线程在更新时说他“进入”内部,则无法预测读者线程是否会将Bob的状态设置为“内部”或“外部”。即使更新线程首先调用该方法,读者线程也可能会获得“外部”状态。

线程不会引起彼此的问题。代码是ThreadSafe。

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