Young's blog Young's blog
首页
Spring
  • 前端文章1

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Young

首页
Spring
  • 前端文章1

    • JavaScript
  • 学习笔记

    • 《JavaScript教程》
    • 《JavaScript高级程序设计》
    • 《ES6 教程》
    • 《Vue》
    • 《React》
    • 《TypeScript 从零实现 axios》
    • 《Git》
    • TypeScript
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 关于 JVM JDK 和 JRE 最详细通俗的解答
  • Java基础之讲透面向对象三大特征
  • 文件读写操作与常用技巧分享
  • 高效快捷读写文件之 RandomAccessFile 类解说
  • java的内存模型
  • 面试官:如何停止一个正在运行的线程?
  • 集合

    • 多线程环境下,HashMap为什么会出现死循环
    • HashMap 源码分析
    • ArrayList 源码分析
    • LinkedHashMap
    • 美团面试拷打:ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?
    • linux卸载自带java JDK,安装配置java jdk环境
    • java 实现LRU 算法
    • Java
    • 集合
    andanyang
    2023-07-25
    目录

    美团面试拷打:ConcurrentHashMap 为何不能插入 null?HashMap 为何可以?

    https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html (opens new window)

    1. 两个类设计者不同,对功能构思想法不同
    2. get 方法,返回的结果为 null 有二义性
      • 值没有在集合中 ;
      • 值本身就是 null。
      • contains(key) 再多线程情况下无法判断 是否真的存在 单线程下可以容忍歧义,而多线程下无法容忍

    整个提问看着非常复杂,其实归纳来说就是两个问题:

    1. ConcurrentHashMap 为什么 key 和 value 不能为 null?
    2. ConcurrentHashMap 能保证复合操作的原子性吗?

    下面我会以此提供这两个问题的详细答案,希望对你有帮助。

    # ConcurrentHashMap 为什么 key 和 value 不能为 null?

    ConcurrentHashMap 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 ConcurrentHashMap 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 ConcurrentHashMap 中的,还是因为找不到对应的键而返回的。

    拿 get 方法取值来说,返回的结果为 null 存在两种情况:

    • 值没有在集合中 ;
    • 值本身就是 null。

    这也就是二义性的由来。

    具体可以参考 ConcurrentHashMap 源码分析( https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html)这篇文章。

    多线程环境下,存在一个线程操作该 ConcurrentHashMap 时,其他的线程将该 ConcurrentHashMap 修改的情况,所以无法通过 containsKey(key) 来判断否存在这个键值对,也就没办法解决二义性问题了。

    与此形成对比的是,HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 HashMap 修改的情况,所以可以通过 contains(key)来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。

    也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。

    如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null。

    public static final Object NULL = new Object();
    
    1

    最后,再分享一下 ConcurrentHashMap 作者本人 (Doug Lea)对于这个问题的回答:

    The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if map.get(key) returns null, you can't detect whether the key explicitly maps to null vs the key isn't mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.

    翻译过来之后的,大致意思还是单线程下可以容忍歧义,而多线程下无法容忍。

    # ConcurrentHashMap 能保证复合操作的原子性吗?

    ConcurrentHashMap 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 HashMap 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了!

    复合操作是指由多个基本操作(如put、get、remove、containsKey等)组成的操作,例如先判断某个键是否存在containsKey(key),然后根据结果进行插入或更新put(key, value)。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。

    例如,有两个线程 A 和 B 同时对 ConcurrentHashMap 进行复合操作,如下:

    // 线程 A
    if (!map.containsKey(key)) {
    map.put(key, value);
    }
    // 线程 B
    if (!map.containsKey(key)) {
    map.put(key, anotherValue);
    }
    
    1
    2
    3
    4
    5
    6
    7
    8

    如果线程 A 和 B 的执行顺序是这样:

    1. 线程 A 判断 map 中不存在 key
    2. 线程 B 判断 map 中不存在 key
    3. 线程 B 将 (key, anotherValue) 插入 map
    4. 线程 A 将 (key, value) 插入 map

    那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。

    那如何保证 ConcurrentHashMap 复合操作的原子性呢?

    ConcurrentHashMap 提供了一些原子性的复合操作,如 putIfAbsent、compute、computeIfAbsent 、computeIfPresent、merge等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。

    上面的代码可以改写为:

    // 线程 A
    map.putIfAbsent(key, value);
    // 线程 B
    map.putIfAbsent(key, anotherValue);
    
    1
    2
    3
    4

    或者:

    // 线程 A
    map.computeIfAbsent(key, k -> value);
    // 线程 B
    map.computeIfAbsent(key, k -> anotherValue);
    
    1
    2
    3
    4

    很多同学可能会说了,这种情况也能加锁同步呀!确实可以,但不建议使用加锁的同步机制,违背了使用 ConcurrentHashMap 的初衷。在使用 ConcurrentHashMap 的时候,尽量使用这些原子性的复合操作方法来保证原子性。

    1. 总结 Java7 中 ConcurrentHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变。 Java8 中的 ConcurrentHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由 Java7 中的 Segment 数组 + HashEntry 数组 + 链表 进化成了 Node 数组 + 链表 / 红黑树,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红黑树,在冲突小于一定数量时又回退链表。 有些同学可能对 Synchronized 的性能存在疑问,其实 Synchronized 锁自从引入锁升级策略后,性能不再是问题,有兴趣的同学可以自己了解下 Synchronized 的锁升级。
    编辑 (opens new window)
    #面试
    上次更新: 2024/04/19, 08:52:45
    LinkedHashMap
    linux卸载自带java JDK,安装配置java jdk环境

    ← LinkedHashMap linux卸载自带java JDK,安装配置java jdk环境→

    最近更新
    01
    idea 热部署插件 JRebel 安装及破解,不生效问题解决
    04-10
    02
    spark中代码的执行位置(Driver or Executer)
    12-12
    03
    大数据技术之 SparkStreaming
    12-12
    更多文章>
    Theme by Vdoing | Copyright © 2019-2024 Young | MIT License
    浙ICP备20002744号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式