首页 文章

ConcurrentHashMap完全安全吗?

提问于
浏览
40

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

“但是,即使所有操作都是线程安全的,检索操作也不需要锁定,并且没有任何支持以阻止所有访问的方式锁定整个表 . 这个类在依赖的程序中与Hashtable完全互操作检索操作(包括get)通常不会阻塞,因此可能与更新操作重叠(包括put和remove) . 检索反映了最近完成的更新操作在发生时的结果“ .

6 回答

  • 6

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

    但是,虽然 ConcurrentHashMapHashMap 的线程安全替代品,但重要的是要意识到如果您正在执行多个操作,则可能必须显着更改代码 . 例如,请使用以下代码:

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

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


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

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

    • 线程 A 调用 containsKey 并发现密钥不存在,但会立即暂停 .

    • 线程 B 调用 containsKey 并发现密钥不存在,并且有时间插入其值 v2 .

    • 线程 A 继续并插入 v1 ,_ "peacefully"覆盖(因为 put 是线程安全的)线程 B 插入的值 .

    现在线程 B "thinks"它已经成功插入了它自己的值 v2 ,但是 Map 包含 v1 . 这确实是一场灾难,因为线程 B 可以调用 v2.updateSomething() 并且"think"表示 Map 的消费者(例如其他线程)可以访问该对象,并且可能会看到可能重要的更新("like: this visitor IP address is trying to perform a DOS, refuse all the requests from now on") . 相反,该对象将很快被垃圾收集和丢失 .

  • 4

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

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

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

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

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

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

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

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

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

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

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

    "Retrieval operations (including get) generally do not block"因为 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);
    }
    
  • 8

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

    • 它不会抛出任何异常,包括 ConcurrentModificationException

    • 它将返回在过去的某个(最近)时间为真的结果 . 这意味着两次背靠背调用get可以返回不同的结果 . 当然,这也适用于任何其他 Map .

  • 4

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

    ConcurrentHashMap 读取几乎不使用同步机制 . 我说几乎是因为它会在获得 null 值时使用同步 . 基于 ConcurrentHashMap 无法存储 null 值的事实(是的,除了键值也不能为空)如果它将获得该值,则必须表示在写入键值对的过程中调用了读取(在输入之后)是为密钥创建的,但它的值尚未设置 - 它是 null ) . 在这种情况下,读取线程需要等到写入完成 .

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

  • 12

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

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

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

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

  • 42

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

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

相关问题