58

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

“然而,尽管所有操作都是线程安全的,但检索操作并不需要锁定,并且不支持以阻止所有访问的方式锁定整个表。这个类与依赖程序中的 Hashtable 完全互操作它的线程安全,但不是它的同步细节。

检索操作(包括 get)一般不会阻塞,因此可能与更新操作(包括 put 和 remove)重叠。检索反映了最近完成的更新操作开始时的结果。”

4

6 回答 6

67

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

然而,虽然ConcurrentHashMap它是一个线程安全替代品HashMap,但重要的是要意识到,如果您正在执行多个操作,您可能必须大幅更改您的代码。例如,使用以下代码:

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

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


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

想象一下有两个线程AB它们将在映射中放置两个不同的值,v1并且v2分别具有相同的键。密钥最初不存在于地图中。它们以这种方式交错:

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

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

于 2013-02-19T00:33:18.657 回答
20

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

此类与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的含义会更清楚:

“检索操作(包括获取)通常不会阻塞”,因为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);
}
于 2013-02-19T02:41:02.330 回答
10

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

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

HashMap被划分为基于 的“桶”hashCodeConcurrentHashMap使用这个事实。它的同步机制是基于阻塞桶而不是整个Map. 这样,很少有线程可以同时写入几个不同的存储桶(一个线程一次可以写入一个存储桶)。

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

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

于 2013-02-19T00:54:03.147 回答
5

ConcurrentHashMap 中的 get() 是线程安全的,因为它读取的是 Volatile 的值。如果任何键的值为 null,则 get() 方法会一直等待,直到它获得锁,然后读取更新的值。

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

于 2013-08-20T05:22:26.920 回答
4

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

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

线程不会导致彼此的问题。代码是线程安全的。

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

于 2013-02-19T00:30:35.053 回答