当前位置: 首页 > news >正文

Java—ConcurrentHashMap

JDK1.7(Segment+ReentrantLock)

ConcurrentHashMap在对象中保存了一个Segment数组。每个Segment元素类似于一个Hashtable;这样,在执行put操作时首先根据hash算法定位到元素属于哪个Segment,然后对该Segment加锁即可,不同的Segment可以并发put。而Segment的锁实现其实是ReentrantLock,同一个Segment读写可以并发。

😃无参构造初始化后:

  • Segment 数组长度为 16,不可以扩容
  • Segment[i] 的默认大小为 2,负载因子是 0.75(超过四分之三才扩容,刚好相等不扩容),得出初始阈值为 1.5,也就是以后插入第一个元素不会触发扩容,插入第二个会进行第一次扩容,扩容为数量翻倍
  • 初始化 segment[0],其他位置还是 null,其它的segment[i]懒加载,通过segment[0]原型进行创建,如果此时segment[0]大小已经是4了,其它segment[i]通过它创建时也是4

😃put流程:

  • 为输入的Key做Hash运算,得到hash
  • 通过hash,定位到对应的Segment对象(根据hash值的高4位(根据Segment数量决定是高几位,如果是32个Segment,就是高5位),找到对应Segment)
  • 获取可重入锁
  • 再次通过hash值,定位到Segment当中数组的具体位置
  • 插入或覆盖HashEntry对象
  • 释放锁

😃get(无锁):

  • 为输入的key做Hash运算,得到hash
  • 通过hash,定位到对应的Segment对象
  • 再次通过hash,定位到Segment当中数组的具体位置

JDK1.8(CAS + synchronized)

有参初始化:

懒汉式初始化桶元素

factor负载因子默认为0.75(只有在初始化时才有用,后面扩容永远根据0.75计算,即使factor构造方法传入为0.5或其它)

capacity表示我想往数组先放capacity个元素,至于数组多大你看着办,比如capacity为16,表明我想先放16个元素,那么初始化大小就会为32(需要为2^n,且不会导致扩容,即初始化大小 > capacity / factor,且初始化大小 = 2 ^ n)

如果capacity为12,factor是0.75,初始化大小就是32

如果capacity是11,factor是0.75,初始化大小就是16

如果capacity是7,factor是0.5,初始化大小就是16

get:无锁获取,因为Node[]Node中的val和next有volatile

put:

  1. 检验key和value不能为null
  2. 进入死循环
  3. 如果第一次添加元素,初始化table,使用CAS保证线程安全,然后重新进入循环
  4. 如果头节点为null,CAS将自己设置为头节点,成功则break,失败则重新进入循环
  5. 如果头节点是ForwardingNode,说明这个桶正在进行扩容,则帮忙进行扩容,然后重新进入循环
  6. 到了这里说明需要往桶下标的非头节点中添加元素,此时使用synchronized,锁对象是头节点
    1. 再次确认链表头节点有没有被移动
      1. 如果是链表,走链表put操作,如果有相同的key则更新,否则添加
      2. 如果是红黑树,走红黑树put操作,如果有相同的key则更新,否则添加
    1. 释放锁,判断是否需要树化,然后break
  1. 使用累加单元数组进行size累加,判断是否需要扩容

扩容:

  • 整个扩容操作分为两个部分:
    • 第一部分是构建一个 nextTable,它的容量是原来的两倍,这个操作是单线程完成的。
    • 第二个部分是将原来 table 中的元素复制到 nextTable 中,主要是遍历复制的过程。遍历是从后往前遍历原table
  • 得到当前遍历的数组位置 i,然后利用 tabAt 方法获得 i 位置的元素:
    • 如果这个位置为空,就在原 table 中的 i 位置放入 forwardNode 节点,这个也是触发并发扩容的关键;
    • 如果这个位置是 Node 节点(fh>=0),并且是链表的头节点,就把这个链表分裂成两个链表,把它们分别放在 nextTable 的 i 和 i+n 的位置上(桶下标不变的留在原地,变的放到 i + n 处);处理完后头节点变成forwardNode(如果是链表,则复制值创建新节点对象扩容,如果是一个节点,则挪动元素扩容)
    • 如果这个位置是 TreeBin 节点(fh<0),也做一个反序处理,并且判断是否需要 untreefi,把处理的结果分别放在 nextTable 的 i 和 i+n 的位置上;处理完后头节点变成forwardNode
    • 遍历所有的节点,就完成复制工作,这时让 nextTable 作为新的 table,并且更新 sizeCtl 为新容量的 0.75 倍 ,完成扩容。

  • 扩容时有其余线程来get:
    • 如果头节点是forwardNode,那么就去新table中get,否则,在原table中查询,又因为迁移链表时是深拷贝扩容,所以get能查找到对应元素,不会出现并发问题

  • 扩容时有其余线程来put:
    • 如果要put的桶下标还不在扩容中,直接put
    • 如果put的桶下标正在扩容中,只能阻塞
    • 如果put的桶下标是forwarding,此时不能往新table中put,因为此时新table还没扩容完成,put可能产生并发问题,此时会去协助扩容

😃为什么HashMap的key和value可以是null?

首先需要知道HashMap的containsKey的逻辑是:根据key获取Node,然后判断Node是否为null,这样子的话就是精准的containsKey!!!

为什么这样子就是精准的containsKey呢?因为如果你是根据key获取value,然后判断value是否为null,如果此时value本身就允许为null而且为null,方法会返回false,你以为map中不存在这个key,但其实存在,只是它的value为null罢了

如果key为null,这时候containsKey(null)的话,如果得不到node节点,说明为null的key不存在,返回false;如果找得到node节点,说明为null的key存在,返回true。也就是说HashMap的containsKey能解决key的null歧义

如果value为null,这时候get(user1),发现获得null值,那么这时候是有两种可能:user1的value就是nulluser1在map中不存在,所以返回null。这时候就产生了歧义。也就是说HashMap的get没解决value为null的歧义!不过这个时候你只需要containsKey(user1)一下就能解决这个歧义。

就是说在无并发下,你key和value都可以为null,一点问题、一点歧义没有

一个是containsKey解决key为null的歧义,一个是get + containsKey解决value为null的歧义

但多线程的情况下你containsKey是解决不了value可以为null的歧义的

比如线程a先get(user1),得到null,这时候就不知道是value为null,还是user1在map中不存在。这时候想再containsKey(user1)确认一下。但此时可能是第一种情况,即user1在map中存在,但value为null,不过线程b又来把这个key为user1的键值对给删了,那你此时containsKey(user1)就返回false,你就以为是第二种情况,这就与之前的事实不符!不过HashMap本身就不是线程安全的,可以容忍这种错误

ConcurrentHashMap的key和value为什么不能是null?

根据HashMap的分析,要让ConcurrentHashMap的key和value允许为null,就要解决并发下value允许为null产生的歧义问题!

这时候就有两种办法:

  • get查询时加锁!保证其它线程不会来删除,但这明显是会影响效率的!这对追求高并发效率的ConcurrentHashMap是肯定不能容忍的
  • 直接不允许value为null!暴力简单高效,ConcurrentHashMap采用了这个方式

当你不允许value为null时,此时key为null是完全可以的,因为containsKey可以解决key为null的歧义

相关文章:

  • JAVA:Web安全防御
  • CSS 记载
  • 客户端本地搭建
  • LeetCode算法题(Go语言实现)_55
  • 蓝桥杯中的知识点
  • 正点原子TFTLCD扩展
  • FreeRTOS-任务的创建删除,挂起与恢复
  • JavaFX深度实践:从零构建高级打地鼠游戏(含多物品与反馈机制)
  • Springboot 集成 RBAC 模型实战指南
  • C++IO流
  • Electron使用WebAssembly实现CRC-32 原理校验
  • 【项目】基于MCP+Tabelstore架构实现知识库答疑系统
  • 测试OMS(订单管理系统)时,对Elasticsearch(ES)数据和算法数据进行测试(如何测试几百万条数据)
  • UDP协议理解
  • 【(保姆级教程)Ubuntu24.10下部署Dify】
  • 【C语言】动态内存的常见错误
  • JavaFX 实战:从零打造一个功能丰富的英文“刽子手”(Hangman)游戏
  • NLP高频面试题(五十一)——LSTM详解
  • 玩转Docker | 使用Docker部署DashMachine个人书签工具
  • 深度学习3.6 softmax回归的从零开始实现
  • 富力地产:广州富力空港假日酒店第一次拍卖流拍,起拍价约2.77亿元
  • 大学2025丨本科专业大调整,教育专家:化解就业难背后供需错配
  • 夸大事实拍视频发网络,镇雄两名网红勒索两千元删帖费被拘
  • “HPV男女共防计划”北半马主题活动新闻发布会在京举办
  • 从沙漠到都市:贝亲世界地球日特别行动,以桃叶冰爽力开启地球降温之旅
  • 马上评|京东VS美团,消费者希望看到的不是“口水仗”