【昕宝爸爸小模块】ConcurrentHashMap为什么不允许null值

在这里插入图片描述

ConcurrentHashMap为什么不允许null值

  • 一、✅典型解析
  • 二、✅要实现一个HashMap怎么做
    • 2.1 ✅需要考虑以下几个方面
    • 2.2 ✅基于数组和链表的HashMap实现Demo
    • 2.3 ✅扩容后如何解决链表长度过长的问题
  • 三、✅拓展知识仓
    • 3.1 ✅在多线程环境下如何保证数据的正确性和性能
    • 3.2 ✅那如何在多线程环境下进行乐观锁


一、✅典型解析


我们知道,ConcurrentHashMap在使用时,和HashMap有一个比较大的区别,那就是HashMap中,null 可以作为键或者值都可以。而在ConcurrentHashMap中,key和value都不允许为null。


那么,为什么呢? 为啥ConcurrentHashMap要设计成这样的呢?


关于这个问题,其实最有发言权的就是ConcurrentHashMap的作者——Doug Lea


他自己曾经出面解释过这个问题,内容如下 http://cs.oswego.edu/pipermail/concurrencyinterest/2006-May/002485.html ,原文地址已经打不开了,大家将就着看一下截图吧)


在这里插入图片描述

我简单解释一下这张图的意思:


ConcurrentMap(如ConcurrentHashMap、ConcurrentSkipListMap) 不允许使用null值的主要原因是,在非并发的Map中(如HashMap),是可以容忍模糊性 (二义性)的,而在并发Map中是无法容忍的


假如说,所有的Map都支持null的话,那么map.get(key) 就可以返回 null,但是,这时候就会存在一个不确定性,当你拿到 null 的时候,你是不知道他是因为本来就存了一个 null 进去还是说就是因为没找到而返回了 null 。


在HashMap中,因为它的设计就是给单线程用的,所以当我们map.get(key)返 null 的时候,我们是可以通过 map.contains(key)检查来进行检测的,如果它返回true,则认为是存了一个null,否则就是因为没找到而返回了null。


但是,像 ConcurrentHashMap ,它是为并发而生的,它是要用在并发场景中的,当我们 map.get(key) 返回 null 的时候,是没办法通过 map.contains(key) 检查来准确的检测,因为在检测过程中可能会被其他线程所修改,而导致检测结果并不可靠。


So,为了让 ConcurrentHashMap 的语义更加准确,不存在二义性的问题,他就不支持 null 。


二、✅要实现一个HashMap怎么做


2.1 ✅需要考虑以下几个方面


  1. 数据结构选择:HashMap基于键值对(key-value pair)的数据结构,因此你需要选择一种合适的数据结构来存储键值对。常用的数据结构有数组、链表、红黑树等。

  2. 哈希函数设计:HashMap通过哈希函数将键映射到桶(bucket)中,因此你需要设计一个高效的哈希函数,以保证键的分布均匀和查询性能。

  3. 桶的设计:为了提高查询性能,你需要为每个桶设计合适的数据结构,如链表、红黑树等。桶的数量需要根据实际需求和内存大小进行权衡。

  4. 同步机制:如果你想让你的HashMap线程安全,你需要考虑使用同步机制,如使用synchronized关键字或ReentrantLock等。

  5. 扩容机制:当HashMap中的元素数量超过一定阈值时,需要考虑进行扩容操作。扩容时需要重新计算哈希函数,并调整桶的数量和数据结构。

实现一个HashMap需要综合考虑数据结构、哈希函数、桶的设计、同步机制和扩容机制等多个方面。如果你不熟悉这些概念和技术,建议使用Java标准库提供的HashMap实现,或者使用其他成熟的第三方库。


2.2 ✅基于数组和链表的HashMap实现Demo


内有逐行注释,就不做解释了!



/**
*  @author xinbaobaba
*/
static class Entry<K,V> implements Map.Entry<K,V> {  
    final K key;  
    V value;  
    Entry<K,V> next;  
    // ... 构造方法和其他方法  
}  
  
// 如果要实现红黑树,还需要定义TreeNode,它是Entry的子类,并且包含红黑树所需的额外字段和方法。  
// 这里为了简化,我们省略TreeNode的定义。  
  
// HashMap的主类定义  
public class HashMap<K,V> {  
    // 初始容量  
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量  
    static final int MAXIMUM_CAPACITY = 1 << 30;  
    // 默认加载因子  
    static final float DEFAULT_LOAD_FACTOR = 0.75f;  
    // 桶数组  
    transient Entry<K,V>[] table;  
    // 存放具体元素的数量  
    transient int size;  
    // 扩容阈值,当size大于threshold就一定会扩容  
    int threshold;  
    // 加载因子  
    final float loadFactor;  
  
    // 构造函数  
    public HashMap(int initialCapacity, float loadFactor) {  
        // 参数校验  
        // 初始化桶数组  
        // 初始化阈值  
        // 初始化加载因子  
    }  
  
    // 哈希函数,可以自定义实现,也可以使用JDK中的实现  
    static final int hash(Object key) {  
        int h;  
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  
    }  
  
    // 根据key的哈希值计算数组索引  
    static int indexFor(int h, int length) {  
        return h & (length - 1);  
    }  
  
    // 实现put方法  
    public V put(K key, V value) {  
        // 如果table为空或者长度为0,就初始化  
        if (table == null || size == 0) {  
            inflateTable(threshold);  
        }  
        // 如果key为null,就放到table[0]的位置  
        if (key == null)  
            return putForNullKey(value);  
        int hash = hash(key);  
        int i = indexFor(hash, table.length);  
        // 遍历链表,如果key已存在,就覆盖掉value  
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
            Object k;  
            if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {  
                V oldValue = e.value;  
                e.value = value;  
                e.recordAccess(this);  
                return oldValue;  
            }  
        }  
        // 如果key不存在,就添加到链表的头部  
        modCount++;  
        addEntry(hash, key, value, i);  
        return null;  
    }  
  
    // 添加新的键值对到链表中  
    void addEntry(int hash, K key, V value, int bucketIndex) {  
        // 如果size大于阈值,就扩容  
        if ((size >= threshold) && (null != table[bucketIndex])) {  
            resize(2 * table.length);  
            hash = (null != key) ? hash(key) : 0;  
            bucketIndex = indexFor(hash, table.length);  
        }  
  
        createEntry(hash, key, value, bucketIndex);  
    }  
  
    // 创建新的Entry并添加到桶中  
    void createEntry(int hash, K key, V value, int bucketIndex) {  
        Entry<K,V> e = table[bucketIndex];  
        // 将新创建的 Entry 放入桶中,并指向原来的 Entry  
        table[bucketIndex] = new Entry<>(hash, key, value, e);  
        size++;  
    }  
  
    // 扩容方法  
    void resize(int newCapacity) {  
        // ... 省略扩容逻辑  
    }  
  
    // 其他方法,如get、remove等  
    // ...  
}

2.3 ✅扩容后如何解决链表长度过长的问题


扩容后解决链表长度过长的问题主要有以下两种方法:

1. 树化:当链表长度大于8并且数组长度大于等于64时,可以将链表转为红黑树。红黑树查找元素的时间复杂度为O(logN),而链表查找元素的时间复杂度为O(N)。这样可以避免在链表超长时性能下降,树化应当是偶然情况。


2. 重新计算哈希值:如果所有元素的哈希值一样,扩容则不能缩短链表长度提高查询效率。这时需要重新计算哈希值,以减少哈希冲突,从而缩短链表长度。


当然,除了上面的树化重新计算哈希值的方法,还有一些其他方法可以解决扩容后链表长度过长的问题:


  1. 调整数组长度:如果数组长度过短,可能导致哈希冲突过多,链表长度过长。可以适当增加数组长度,减少哈希冲突,从而缩短链表长度。

  2. 使用开放地址法:当发生哈希冲突时,可以使用开放地址法(例如线性探测)来处理。这种方法可以在发生冲突时重新计算哈希值,减少链表长度。


  1. 使用再哈希:在某些情况下,可以使用再哈希技术,即在计算哈希值时使用多个哈希函数,以获得更好的分布。这有助于减少链表长度。

  2. 链表合并:在扩容时,可以将相邻的链表合并,以减少链表数量。这种方法适用于链表长度差异较大的情况。

以上可以结合使用,以更好地解决扩容后链表长度过长的问题。具体选择哪种方法取决于具体的应用场景和数据特点。


注意:这些技术通常在需要处理大量数据和复杂数据结构的场景下比较常见,例如数据库索引、搜索引擎、缓存系统等。在这些场景下,需要快速地查找、插入和删除数据,而链表和哈希表等数据结构可以提供高效的性能。通过使用这些技术,可以有效地解决数据量大、哈希冲突多等问题,提高系统的性能和响应速度。


看一个案例,一段代码使用哈希表和链表来存储用户信息,并提供添加、删除和查找用户的方法:

import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
/**
*   @author xinbaobaba
*/  
public class UserService {  
	// 用户映射表,以用户ID为键,用户对象为值  
    private Map<String, User> userMap;  
    // 链表的头节点,用于存储用户ID的链表结构
    private ListNode head;  
  
    public UserService() {
    	// 初始化用户映射表   
        userMap = new HashMap<>(); 
         // 初始化链表头节点为空
        head = null;  
    }  
  
    public void addUser(User user) {
    	// 获取用户ID  
        String userId = user.getId();
        // 如果用户映射表中不存在该用户ID  
        if (!userMap.containsKey(userId)) {
        	// 将用户添加到映射表中  
            userMap.put(userId, user);
            // 如果链表为空  
            if (head == null) {
            	// 创建新的链表节点,并设置其为头节点    
                head = new ListNode(userId);  
            } 
			// 如果链表不为空
			else {
				// 从链表头节点开始遍历  
                ListNode curr = head;
                 // 遍历到最后一个节点前   
                while (curr.next != null) {
                	// 移动到下一个节点  
                    curr = curr.next;  
                }
                // 在最后一个节点后面添加新的节点  
                curr.next = new ListNode(userId);  
            }  
        }
		// 如果用户映射表中已存在该用户ID
		 else {
		 	//输出提示的信息  User already exists(用户已存在)
            System.out.println("User already exists.");  
        }  
    }  
  	// 删除用户的方法 (一样的道理,不逐行注释了)
    public void removeUser(String userId) {  
        if (userMap.containsKey(userId)) {  
            User removedUser = userMap.remove(userId);  
            ListNode removedNode = null;  
            if (head != null && head.val.equals(userId)) {  
                removedNode = head;  
                head = head.next;  
            } else {  
                ListNode curr = head;  
                while (curr != null && curr.next != null && curr.next.val.equals(userId)) {  
                    removedNode = curr.next;  
                    curr.next = curr.next.next;  
                }  
            }  
            if (removedNode != null) {  
                // do something with removedUser and removedNode, e.g., free memory or update related data structures  
            } else {  
                System.out.println("User not found.");  
            }  
        } else {  
            System.out.println("User not found.");  
        }  
    }  
    
  	 // 根据用户ID查找用户的方法  
    public User findUserById(String userId) {  
        if (userMap.containsKey(userId)) {  
            return userMap.get(userId);  
        } else {  
            return null; // or throw an exception if the user is required to exist  
        }  
    }  
    
    // 批量添加用户的方法(用于一次性添加多个用户) 
    public void addUsers(List<User> users) {  
        for (User user : users) {  
            addUser(user);  
        }  
    }  
     //remove 
    public void removeUsers(List<String> userIds) {  
        for (String userId : userIds) {  
            removeUser(userId);  
        }  
    }  
      
    public List<User> getAllUsers() {  
        List<User> users = new ArrayList<>();  
        ListNode curr = head;  
        while (curr != null) {  
            users.add(userMap.get(curr.val));  
            curr = curr.next;  
        }  
        return users;  
    }  
      
    public int getUserCount() {  
        return userMap.size();  
    }  
      
    // Other methods and attributes can be added here...  
}

三、✅拓展知识仓


3.1 ✅在多线程环境下如何保证数据的正确性和性能


在多线程环境下,要保证数据的正确性和性能,可以考虑以下几个方面:

  1. 线程安全:选择线程安全的数据结构或使用同步机制,如synchronized关键字或ReentrantLock等,保证多个线程对数据的操作不会产生冲突或不一致。

  2. 原子操作:对于关键的更新操作,使用原子操作,如AtomicInteger、AtomicBoolean等,保证操作的原子性,避免数据竞争和线程安全问题。

  3. 乐观锁:使用乐观锁机制,如CAS(Compare-and-Swap)操作,在更新数据时比较版本号,只有当版本号一致时才进行更新,否则重试直到成功。

  4. 数据同步:对于共享数据,使用同步机制进行数据同步,保证多个线程对数据的操作顺序一致,避免数据不一致问题。
  5. 避免死锁:在实现多线程程序时,需要避免死锁情况的发生。可以采用一些策略来预防和解决死锁问题,如避免循环等待、按顺序获取锁、设置锁的超时时间等。

  6. 合理使用锁粒度:根据实际情况选择合适的锁粒度,避免过度同步导致性能下降。可以考虑使用细粒度锁或分段锁等策略。

  7. 性能优化:对于关键路径上的操作,需要进行性能优化,如使用缓存、减少锁的持有时间、使用低延迟的数据结构等。

  8. 监控和调优:在多线程程序运行过程中,需要监控程序的性能指标和资源使用情况,及时发现和解决性能瓶颈和问题。

要保证数据的正确性和性能,需要综合考虑线程安全、原子操作、乐观锁、数据同步、避免死锁、合理使用锁粒度、性能优化和监控调优等方面。同时需要结合实际业务场景和需求进行权衡和优化。


3.2 ✅那如何在多线程环境下进行乐观锁


在多线程环境下进行乐观锁,可以使用CAS(Compare-and-Swap)操作。CAS操作是一种原子性操作,用于在多线程环境中实现乐观锁。

CAS操作包含三个参数:内存位置V、预期的原值A和新值B。在执行CAS操作时,如果内存位置V的值等于预期原值A,则将内存位置V的值更新为新值B。如果内存位置V的值不等于预期原值A,则说明该内存位置V的值已经被其他线程修改过,此时不会进行更新操作,并且会返回一个失败的结果


在Java中,可以使用 AtomicStampedReference类AtomicMarkableReference 类来实现乐观锁。这些类提供了compareAndSet()方法,该方法接受两个参数:预期的原值和新值。如果当前值与预期原值相匹配,则将当前值更新为新值,并返回true;否则,不做任何操作并返回false。


下面是一个简单的示例代码,演示如何在Java中使用AtomicStampedReference 类实现乐观锁:


import java.util.concurrent.atomic.AtomicStampedReference;

public class OptimisticLocker {
    private AtomicStampedReference<Integer> stampRef = new AtomicStampedReference<>(0, 0);

    public void increment() {
        int stamp = stampRef.getStamp(); // 获取当前时间戳
        int value = stampRef.getValue(); // 获取当前值
        int newValue = value + 1; // 计算新值
        int nextStamp = stamp + 1; // 计算下一个时间戳
        if (stampRef.compareAndSet(value, newValue, stamp, nextStamp)) {
            // 如果更新成功,则打印新值和时间戳
            System.out.println("New value: " + newValue + ", Stamp: " + nextStamp);
        } else {
            // 如果更新失败,则打印当前值和时间戳
            System.out.println("Current value: " + value + ", Stamp: " + stamp);
        }
    }
}

在上面的示例中,我们使用AtomicStampedReference类来存储一个整数值和一个时间戳。在increment()方法中,我们首先获取当前的时间戳和值,然后计算新值和下一个时间戳。接下来,我们使用compareAndSet()方法尝试更新值和时间戳。如果更新成功,则打印新值和时间戳;否则,打印当前值和时间戳。这样就可以实现乐观锁的效果。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

git 的安装

git 的安装 在我们开始使用 Git 前&#xff0c;需要将它安装在我们的电脑上。即便已经安装&#xff0c;最好将它升级到最新的版本。 我们可以通过软件包或者其它安装程序来安装&#xff0c;或者下载源码编译安装。 本文只介绍通过在 windows 上安装软件包的方式&#xff0c;其…

vue Element Plus Cascader级联选择器点击标签选中复选框

element-plus原功能 element-plus的Cascader级联选择器点击标签时是不会选中复选框的&#xff0c;我们想要实现点击标签时也能选中复选框这个效果&#xff0c;那么就要用到一些原生的方法 实现效果 mounted() {// Cascader 级联选择器: 点击文本就让它自动点击前面的input就可…

Vue 自定义仿word表单录入之日期输入组件

因项目需要&#xff0c;要实现仿word方式录入数据&#xff0c;要实现鼠标经过时才显示编辑组件&#xff0c;预览及离开后则显示具体的文字。 鼠标经过时显示 正常显示及离开时显示 组件代码 <template ><div class"paper-input flex flex-col border-box "…

JAVA基础学习笔记-day17-反射

JAVA基础学习笔记-day17-反射 1. 反射(Reflection)的概念1.1 反射的出现背景1.2 反射概述1.3 Java反射机制研究及应用1.4 反射相关的主要API1.5 反射的优缺点 2. 理解Class类并获取Class实例2.1 理解Class2.1.1 理论上2.1.2 内存结构上 2.2 获取Class类的实例(四种方法)2.3 哪些…

Android基于Matrix绘制PaintDrawable设置BitmapShader,以手指触点为中心显示原图像圆图,Kotlin

Android基于Matrix绘制PaintDrawable设置BitmapShader&#xff0c;以手指触点为中心显示原图像圆图&#xff0c;Kotlin 手指在上面的图上移动&#xff0c;“剪切”出上面图中以手指触点为中心的图&#xff08;半径图&#xff09;&#xff0c;然后在下面的ImageView显示。 impor…

【AI视野·今日Robot 机器人论文速览 第七十三期】Tue, 9 Jan 2024

AI视野今日CS.Robotics 机器人学论文速览 Tue, 9 Jan 2024 Totally 40 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Robotics Papers Digital Twin for Autonomous Surface Vessels for Safe Maritime Navigation Authors Daniel Menges, Andreas Von Brandis, A…

鸿蒙原生应用再添新丁!万达 入局鸿蒙

鸿蒙原生应用再添新丁&#xff01;万达 入局鸿蒙 来自 HarmonyOS 微博1月11日消息&#xff0c;#万达酒店及度假村启动鸿蒙原生应用及元服务开发# 作为具有中国特色的国牌服务酒店标杆之一&#xff0c;万达酒店及度假村Wanda 将带来全新的服务和交互方式&#xff0c;一步获取“…

3 快速前端开发

3 前端JavaScript 3 前端JavaScript1. JavaScript1.1 代码位置1.2 注释1.3 变量1.4 字符串类型案例&#xff1a;跑马灯 1.5 数组案例&#xff1a;动态数据 1.6 对象&#xff08;字典&#xff09;案例&#xff1a;动态表格 1.7 条件语句1.8 函数 2.DOM2.1 事件的绑定 3.知识点的…

大模型训练营Day3 基于 InternLM 和 LangChain 搭建你的知识库

本次的授课人是一个提示词开发项目的负责人。下面一起进入本期课程吧》 本次课程内容主要如下&#xff1a; 开篇交代了大模型的局限性&#xff0c;然后引出主题&#xff1a; 简单总结&#xff0c;大模型是根据数据集训练&#xff0c;很难使用具有实时性的数据进行重新训练&am…

SpringBoot中使用LocalDateTime踩坑记录

文章目录 前言一、为什么推荐使用java.time包的LocalDateTime而不是java.util的Date&#xff1f;二、使用LocalDateTime和LocalDate时遇到了哪些坑&#xff1f;2.1 Redis序列化报错2.1.1 问题现象2.1.2 问题分析2.1.3 解决方案 2.2 LocalDateTime和LocalDate类型的属性返回给前…

Spark SQL进阶

DataFrame详解 清洗相关API 去重API 删除空缺值的API 替换缺失值的API from pyspark import SparkConf, SparkContext import os from pyspark.sql import SparkSession# 绑定指定的Python解释器 os.environ[SPARK_HOME] /export/server/spark os.environ[PYSPARK_PYTHON]…

函数——自制函数(c++)

今天进入自制函数。 自制函数&#xff0c;需要自己定义其功能。比如&#xff0c;设置一个没有参数没有返回值的积木&#xff0c;叫“aaa”。那么&#xff0c;如果想要运行“aaa”&#xff0c;就需要以下代码&#xff1a; void aaa(); 告诉系统有“aaa”…

强化学习的数学原理学习笔记 - Actor-Critic

文章目录 概览&#xff1a;RL方法分类Actor-CriticBasic actor-critic / QAC&#x1f7e6;A2C (Advantage actor-critic)Off-policy AC&#x1f7e1;重要性采样&#xff08;Importance Sampling&#xff09;Off-policy PGOff-policy AC &#x1f7e6;DPG (Deterministic AC) 本…

【自控实验】1. 线性系统串联超前校正实验

本科课程实验报告&#xff0c;有太多公式和图片了&#xff0c;干脆直接转成图片了 仅分享和记录&#xff0c;不保证全对 串联超前校正实验&#xff1a;频域设计计算(校正装置)&#xff0c;时域观察验证(校正结果) 使用matlab中的simulink进行仿真

String有没有最大长度限制?

大家都用过String字符串&#xff0c;有的人可能还不知道它的长度在某些方面是有一些限制。 public String(byte bytes[], int offset, int length);这是java.lang.String中的一个构造函数&#xff0c;可以看到它的长度是int类型&#xff0c;int的最大取值是2^31-1.但是我们却不…

python股票分析挖掘预测技术指标知识跳空缺口指标详解(5)

本人股市多年的老韭菜&#xff0c;各种股票分析书籍&#xff0c;技术指标书籍阅历无数&#xff0c;萌发想法&#xff0c;何不自己开发个股票预测分析软件&#xff0c;选择python因为够强大&#xff0c;它提供了很多高效便捷的数据分析工具包。 我们已经初步的接触与学习其中数…

Next.js 学习笔记(五)——渲染

渲染 渲染将你编写的代码转换到用户界面。React 和 Next.js 允许你创建混合 web 应用程序&#xff0c;其中部分代码可以在服务器或客户端上呈现。本节将帮助你了解这些渲染环境、策略和运行时之间的差异。 基本知识 首先&#xff0c;下列对熟悉三个基本的网络概念是有帮助的…

哈希-力扣454.四数相加Ⅱ

题目 给你四个整数数组 nums1、nums2、nums3 和 nums4 &#xff0c;数组长度都是 n &#xff0c;请你计算有多少个元组 (i, j, k, l) 能满足&#xff1a; 0 < i, j, k, l < nnums1[i] nums2[j] nums3[k] nums4[l] 0 示例 1&#xff1a; 输入&#xff1a;nums1 [1…

UI功能6大流程、接口测试8大流程这些你真的全会了吗?

在讲接口流程测试之前&#xff0c;首先需要给大家申明下&#xff1a;接口测试对于测试人员而言&#xff0c;非常非常重要&#xff0c;懂功能测试接口测试&#xff0c;就能在企业中拿到一份非常不错的薪资。 这么重要的接口测试&#xff0c;一般也是面试笔试必问。为方便大家更…
最新文章