ConcurrentHashMap完全安全吗?

use*_*911 48 java multithreading synchronized java.util.concurrent

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

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

检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和remove)重叠.检索反映了最近完成的更新操作的结果."

gd1*_*gd1 51

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

但是,尽管ConcurrentHashMap是一个线程安全替代品HashMap,但重要的是要意识到如果您正在进行多个操作,则可能需要显着更改代码.例如,请使用以下代码:

if (!map.containsKey(key)) 
   return map.put(key, value);
else
   return map.get(key);
Run Code Online (Sandbox Code Playgroud)

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


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

想象一下,有两个线程A,B它们将在地图中放置两个不同的值,v1v2分别具有相同的键.密钥最初不在地图中.它们以这种方式交错:

  • 线程A调用containsKey并发现密钥不存在,但会立即暂停.
  • 线程B调用containsKey并发现密钥不存在,并有时间插入其值v2.
  • 线程A恢复并插入v1,"和平"覆盖put线程插入的值(因为线程安全)B.

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

  • 我会重新说它"虽然`ConcurrentHashMap`是HashMap的线程安全替代品,但重要的是要意识到如果你正在做多个操作......" (3认同)

Adr*_*hum 17

它是线程安全的.但是,它的线程安全方式可能不是您所期望的.您可以从以下方面看到一些"提示":

此类与Hashtable依赖于其线程安全但不依赖于其同步细节的程序完全可互操作

要以更完整的图片了解整个故事,您需要了解ConcurrentMap界面.

原始Map提供了一些非常基本的读/更新方法.即使我能够进行线程安全的实现Map; 有很多情况下人们不考虑我的同步机制就无法使用我的Map.这是一个典型的例子:

if (!threadSafeMap.containsKey(key)) {
   threadSafeMap.put(key, value);
}
Run Code Online (Sandbox Code Playgroud)

这段代码不是线程安全的,即使地图本身也是如此.两个线程同时调用containsKey()可能会认为没有这样的密钥,因此它们都插入到Map.

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

synchronized(threadSafeMap) {
    if (!threadSafeMap.containsKey(key)) {
       threadSafeMap.put(key, value);
    }
}
Run Code Online (Sandbox Code Playgroud)

这些额外的代码需要您了解地图的"同步细节".在上面的例子中,我们需要知道同步是通过"synchronized"实现的.

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

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

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

该表在内部进行分区,以尝试允许指定数量的并发更新而不会发生争用

检索非阻塞,甚至更新都可以同时发生.但是,非阻塞/并发更新并不意味着它是线程UNsafe.它只是意味着它使用除了简单的"同步"之外的一些方法来实现线程安全.

但是,由于未公开内部同步机制,如果要执行除提供的操作之外的某些复杂操作ConcurrentMap,则可能需要考虑更改逻辑或考虑不使用ConcurrentHashMap.例如:

// only remove if both key1 and key2 exists
if (map.containsKey(key1) && map.containsKey(key2)) {
    map.remove(key1);
    map.remove(key2);
}
Run Code Online (Sandbox Code Playgroud)


Mis*_*ble 10

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

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

  • 如果有一个介入的`put()`或其他修改地图的操作,那么背靠背调用get可以返回不同的结果. (2认同)
  • 我不太同意你的答案,因为:1.不抛出任何异常并不意味着它是线程安全的,并且ConcurrentModificationException不是关于线程安全的.2."两次背对背呼叫的结果相同"绝不是Map的合同,也与线程安全无关. (2认同)

Psh*_*emo 7

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

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

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


AKS*_*AKS 5

ConcurrentHashMap中的get()是线程安全的,因为它读取的是Volatile值.在任何键的值为null的情况下,get()方法等待直到获得锁定然后它读取更新的值.

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