什么?你不知道 ConcurrentHashMap 的 kv 不能为 null?

news2024/11/24 22:51:11

一、背景

最近设计某个类库时使用了 ConcurrentHashMap 最后遇到了 value 为 null 时报了空指针异常的坑。
什么?.png
本文想探讨下以下几个问题:
(1) Map接口的常见子类的 kv 对 null 的支持情况。
(2)为什么 ConcurrentHashMap 不支持 key 和 value 为 null?
(3)如果 value 可能为 null ,该如何处理?
(4)有哪些线程安全的 Java Map 类?
(5) 常见的 Map 接口的子类,如 HashMapTreeMapConcurrentHashMapConcurrentSkipListMap 的使用场景。

二、探究

2.1 Map接口的常见子类的 kv 对 null 的支持情况

下图来源于孤尽老师 《码出高效》 第 6 章 数据结构与集合
Xnip2023-03-10_20-29-05.png

2.2 为什么 ConcurrentHashMap 不支持 key 和 value 为 null?

java.util.concurrent.ConcurrentHashMap#put 方法的注释和源码中可以非常容易得看出,不支持 key 和 value null。

    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        int hash = spread(key.hashCode());
        // 省略其他
    }

那么,为什么不支持 key 和 value 为 null 呢?
据查阅资料,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 允许存放 null 值,那么当一个线程调用 get(key) 方法时,返回 null 可能有两种情况:
(1) 一种是这个 key 不存在于 map 中
(2) 另一种是这个 key 存在于 map 中,但是它的值为 null。
这样就会导致线程无法判断这个 null 是什么意思。
在非并发的场景下,可以通过 map.contains(key)检查是否包括该 key,从而断定是不存在 key 还是存在key 但值为 null,但是在并发场景下,判断后调用其他 api 之间 map 的数据已经发生了变化,无法保证对同一个 key 操作的一致性。

2.3 怎么解决?

2.3.1 封装 put 方法,使用前判断

建议封装 put 方法,统一使用该方法对 ConcurrentHashMap 的 put 操作进行封装,当 value 为 null 时,直接 return 即可。

Map<String, Person> map = new ConcurrentHashMap<>();


// 封装 put 操作,为 null 时返回
private void putPerson(String key, Person value){
    if(value == null){
        return;
    }
    map.put(key, value);
}

2.3.2 使用 Optional 类型

使用 Optional

// 创建一个 ConcurrentHashMap<String, Optional<String>>
Map<String, Optional<String>> map = new ConcurrentHashMap<>();

// 插入或更新 key-value 对
map.computeIfAbsent("name", k -> Optional.ofNullable("Alice")); // 如果 name 不存在,则插入 ("name", Optional.of("Alice"))
map.computeIfAbsent("age", k -> Optional.ofNullable(null)); // 如果 age 不存在,则插入 ("age", Optional.empty())

// 获取 value
Optional<String> name = map.get("name"); // 返回 Optional.of("Alice")
Optional<String> age = map.get("age"); // 返回 Optional.empty()
Optional<String> gender = map.get("gender"); // 返回 null

2.3.3 自定义一个表示 null 的类

自定义表示 null 的类, 然后对 put 和 get 操作进行二次封装,参考代码如下:

// 定义一个表示 null 的类
public class NullValue extends Person{
    
}


// 创建一个 ConcurrentHashMap<String, Object>
private Map<String, Person> map = new ConcurrentHashMap<>();

private static final NullValue nullValue = new NullValue();

//使用示例: 值不为 null 时
putPerson("1002", new Person("张三"));

//使用示例: 值为 null 时
putPerson("1003", null);


// 封装设置操作
private void putPerson(String key,Person person){
    if(person == null){
        map.put(key, nullValue);
        return;
    }
   map.put(key, person);
}


// 封装获取操作
private Person getPerson(String key){
    if(key == null){
        return;
    }
    Person person = map.get(key);
    if(person instanceof NullValue){
        return null;
    }
    return person;
}


2.3.4 使用其他线程安全的 Java Map 类

Java 中也有支持 key 和 value 为 null 的线程安全的集合类,比如 ConcurrentSkipListMap (JDK) 和 CopyOnWriteMap (三方)

  • ConcurrentSkipListMap 是一个基于跳表的线程安全的 map,它使用锁分段的技术来提高并发性能。它允许 key 和 value 为 null,但是它要求 key 必须实现 Comparable 接口或者提供一个 Comparator
  • CopyOnWriteMap 是一个基于数组的线程安全的 map,它使用写时复制的策略来保证并发访问的正确性。它允许 key 和 value 为 null。

注意 JDK 中没有提供 CopyOnWriteMap,很多三方类库提供了对应的工具类。如org.apache.kafka.common.utils.CopyOnWriteMap

2.4 常见的 Map 接口的子类的使用场景

Map 接口有很多子类,那么他们各自的适用场景是怎样的呢?
Xnip2023-03-10_20-30-43.png

使用场景主要取决于以下几个方面:

  • 是否需要线程安全:如果需要在多线程环境下操作 Map,那么应该使用 ConcurrentHashMapConcurrentSkipListMap,它们都是并发安全的。而 HashMapTreeMapHashTableLinkedHashMap则不是,并且 HashTable已经被 ConcurrentHashMap取代。
  • 是否需要保证键的顺序:如果需要按照键的自然顺序或者插入顺序遍历 Map,那么应该使用 TreeMap或者 LinkedHashMap,它们都是有序的。而 ConcurrentSkipListMap也是有序的,并且支持范围查询。其他类则是无序的。
  • 是否需要高效地访问和修改:如果需要快速地获取和更新 Map中的元素,那么应该使用 HashMap或者 ConcurrentHashMap,它们都是基于散列函数实现的,具有较高的性能。
    TreeMapConcurrentSkipListMap则是基于平衡树实现的,具有较低的性能。CopyOnWriteMap 则是基于数组实现的,并发写操作会复制整个数组,因此写操作开销很大。

在选择合适的 Map 接口实现时,需要根据具体需求和场景进行权衡。

三、总结

基本功很重要,有时候基本功不扎实,更容易遇到一些奇奇怪怪的坑。假设你不了解 ConcurrentHashMap 的 kv 不能为 null, 测试的时候没有覆盖这种场景,等上线以后遇到这个问题可能直接导致线上问题,甚至线上故障。

ConcurrentHashMap 作者在 put 方法注释中给出了 kv 不允许为 null 的提示,并没有在注释中给出设计原因,给众多读者带来了诸多困惑。这也给我们很大的启发,当我们的某些设计容易引起别人的困惑和好奇时,不仅要将注意事项放在注释中,更应该将设计原因放在注释里,避免给使用者带来困扰

“适合自己的才是最好的”。正如不同的 Map 实现类各有千秋,使用场景各有不同,我们需要根据具体需求和场景进行权衡一样,我们在设计方案时也会遇到类似的场景,我们能做的是根据场景选择最适合的方案。

我们遇到的任何问题,都是彻底掌握某个知识的绝佳机会。当我们遇到问题时,应该主动掌握相关知识,希望大家不仅能够知其然,还要知其所以然。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/403614.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ethercat学习-电机调试问题总结

文章目录问题1&#xff1a;初始化不进入OP状态问题2&#xff1a;PDO通讯数据不对主站硬件&#xff1a;STM32F405LAN8720A主站软件&#xff1a;SOEM 问题1&#xff1a;初始化不进入OP状态 现象描述&#xff1a;主站初始化过程中&#xff0c;打印信息显示状态一直在safe-op&…

多态且原理

多态 文章目录多态多态的定义和条件协变&#xff08;父类和子类的返回值类型不同&#xff09;函数隐藏和虚函数重写的比较析构函数的重写关键字final和override抽象类多态的原理单继承和多继承的虚函数表单继承下的虚函数表多继承下的虚函数表多态的定义和条件 定义&#xff1…

Python批量爬取游戏卡牌信息

文章目录前言一、需求二、分析三、处理四、运行结果前言 本系列文章来源于真实的需求本系列文章你来提我来做本系列文章仅供学习参考阅读人群&#xff1a;有Python基础、Scrapy框架基础 一、需求 全站爬取游戏卡牌信息 二、分析 查看网页源代码&#xff0c;图片资源是否存在…

Node.js + MongoDB 搭建博客 -- 登录页面

准备工作 安装Node.js安装express等相关库MongoDB数据库电脑系统&#xff1a;win11 功能分析 搭建一个简单的具有多人注册、登录、发表文章以及登出功能的博客。 设计目标 未登录&#xff1a;主页左侧导航栏显示home、login、register&#xff0c;右侧显示已发表的文章、发…

视觉SLAM14讲第三章习题作业

这是本人的解答&#xff0c;并非官方解答 验证旋转矩阵是正交矩阵 在第44页中&#xff0c;旋转矩阵的引入是这样的&#xff1a; 所以&#xff0c;我们需要验证矩阵 R[e1Te1′e1Te2′e1Te3′e2Te1′e2Te2′e2Te3′e3Te1′e3Te2′e3Te3′]R \begin{bmatrix} e_1^{T}e_1^{}&am…

【Java开发】设计模式 02:工厂模式

1 工厂模式介绍工厂模式&#xff08;Factory Pattern&#xff09;是 Java 中最常用的设计模式之一。这种类型的设计模式属于创建型模式&#xff0c;它提供了一种创建对象的最佳方式。在工厂模式中&#xff0c;我们在创建对象时不会对客户端暴露创建逻辑&#xff0c;并且是通过使…

Weblogic管理控制台未授权远程命令执行漏洞复现(cve-2020-14882/cve-2020-14883)

目录漏洞描述影响版本漏洞复现权限绕过漏洞远程命令执行声明&#xff1a;本文仅供学习参考&#xff0c;其中涉及的一切资源均来源于网络&#xff0c;请勿用于任何非法行为&#xff0c;否则您将自行承担相应后果&#xff0c;本人不承担任何法律及连带责任。 漏洞描述 Weblogic…

CorelDRAW2023最新版新增功能200多个新模板

CorelDRAW是一款平面矢量绘图排版软件&#xff0c;CorelDRAW运用涵盖企业VI设计&#xff0c;广告设计&#xff0c;包装设计&#xff0c;画册设计&#xff0c;海报、招贴设计&#xff0c;UI界面设计&#xff0c;网页设计&#xff0c;书籍装帧设计&#xff0c;插画设计&#xff0…

韩信点兵问题,鸡兔同笼问题,闰年判断问题等,我用Python瞬间搞定(13)

小朋友们好&#xff0c;大朋友们好&#xff01;我是猫妹&#xff0c;一名爱上Python编程的小学生。欢迎和猫妹一起&#xff0c;趣味学Python。今日主题最近猫妹一直在练习Python编程&#xff0c;有些习题真是经典啊&#xff01;比如韩信点兵问题&#xff0c;比如鸡兔同笼问题等…

【Linux】线程实例 | 简单线程池

今天来写一个简单版本的线程池 1.啥是线程池 池塘&#xff0c;顾名思义&#xff0c;线程池就是一个有很多线程的容器。 我们只需要把任务交到这个线程的池子里面&#xff0c;其就能帮我们多线程执行任务&#xff0c;计算出结果。 与阻塞队列不同的是&#xff0c;线程池中内有…

代码随想录刷题-数组-螺旋矩阵II

文章目录螺旋矩阵 II习题我的解法别人的解法螺旋矩阵 II 本节对应代码随想录中&#xff1a;代码随想录&#xff0c;讲解视频&#xff1a;一入循环深似海 | LeetCode&#xff1a;59.螺旋矩阵II_哔哩哔哩_bilibili 习题 题目链接&#xff1a;59. 螺旋矩阵 II - 力扣&#xff0…

将spring boot项目打包成一个可执行的jar包

将spring boot项目打包成一个可执行的jar包&#xff0c;jar包自带了整个运行环境简化配置&#xff0c;可直接运行&#xff0c;本次使用HelloWorld项目演示。 创建Spring Boot项目在pom.xml中导入插件 <build><plugins><plugin><groupId>org.springfr…

计算机网络:移动IP

移动IP相关概念 移动IP技术是移动结点&#xff08;计算机/服务器&#xff09;以固体的网络IP地址&#xff0c;实现跨越不同网段的漫游功能&#xff0c;并保证了基于网络IP的网络权限在漫游中不发生任何改变。移动结点&#xff1a;具有永久IP地址的设备。归属代理&#xff08;本…

西瓜播放器全屏功能源码分析

H5业务中使用了西瓜播放器&#xff0c;嵌入各个APP&#xff0c;全屏时候会有相应的差异。带着好奇心&#xff0c;阅读一下xgpplayer全屏功能部分源代码。源码地址一、工具方法其他方法看名称就知道是dom操作相关&#xff0c;无需多说&#xff0c;上面这两个工具方法主要用来检测…

数学建模资料整理

数学建模中有三类团队&#xff1a; 第一类&#xff1a;拿到题目&#xff0c;讨论&#xff0c;然后建模手开始建模&#xff0c;编程手开始处理数据&#xff0c;写作手开始写作。 第二类&#xff1a;拿到题目&#xff0c;团内大佬&#xff0c;开始建模&#xff0c;然后编程&#…

【GAOPS055】verilog 乘法、除法和取余

乘法硬件原理结论思路1思路2举例编码仿真综合除法硬件原理verilog代码仿真结果资源占用乘法硬件原理 结论 可以将乘法A x B转为A的移位相加。 利用乘2n就是左移n位的特性乘2^n就是左移n位的特性乘2n就是左移n位的特性&#xff0c;将数拆分为2n2^n2n表示 思路1 原始列竖式计…

【C++】通过priority_queue、reverse_iterator加深对于适配器和仿函数的理解

苦尽甘来 文章目录一、仿函数&#xff08;仿函数就是一个封装()运算符重载的类&#xff09;1.C语言的函数指针2.C的仿函数对象二、priority_queue中的仿函数1.模拟实现优先级队列1.1 优先级队列的本质&#xff08;底层容器为vector的适配器&#xff09;1.2 向下调整算法建堆1.3…

git添加子模块(submodule)

git添加子模块(submodule) 背景 有时候自己的项目需要用到别人的开源代码&#xff0c;例如 freertos 和 tinyusb 这个时候有两种选择 将开源的代码下载下来放到自己的 git 中管理 缺点&#xff1a;如果远端仓库更新&#xff0c;自己仓库的代码不会更新 将开源代码通过子模块…

2023河北沃克HEGERLS甘肃金昌重型仓储项目案例|托盘式四向穿梭车智能密集存储系统在工业行业的创新应用

项目名称&#xff1a;自动化仓储托盘式四向穿梭车智能密集立体库项目 项目合作客户&#xff1a;甘肃省金昌市某集团企业 项目施工地域&#xff1a;甘肃省金昌市 设计与承建单位&#xff1a;河北沃克金属制品有限公司&#xff08;自主品牌&#xff1a;海格里斯HEGERLS&#x…

Mysql的Explain关键字

引入 MySQL为我们提供了 explain 关键字来直观的查看一条SQL的执行计划 概念 1.id SELECT识别符&#xff0c;这是SELECT的查询序列号 2.select_type 查询类型 SIMPLE:简单SELECT(不使用UNION或子查询) PRIMARY:最外面的SELECT UNION:UNION中的第二个或后面的SELECT语句 DEPEND…