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

ConcurrentHashMap 超详细详解

ConcurrentHashMap 超详细详解

一、基础定位

ConcurrentHashMap是 Java 线程安全的哈希表,替代老旧线程安全集合Hashtable,同时弥补HashMap并发下死循环、数据丢失问题:

  1. HashMap:并发读写会链表循环、数据覆盖,线程不安全;
  2. Hashtable:全表加synchronized锁,粒度极大,并发性能极差;
  3. ConcurrentHashMap:分段锁 / 分段 CAS+synchronized,锁粒度极小,高并发读写性能优秀。

版本区分核心:

  • JDK7:分段锁 Segment,底层数组 + 链表,多段独立锁;
  • JDK8/JDK9/JDK17:取消 Segment,CAS + 节点 synchronized,底层数组 + 链表 / 红黑树,锁粒度降到哈希桶单个节点。

二、JDK 7 实现原理(过时,仅做对比理解)

1. 底层结构

plaintext

[Segment[HashEntry链表], Segment[], ...]
  1. 外层:Segment[]分段数组,默认 16 段,每段独立锁;
  2. 内层:每个 Segment 内部是HashEntry<K,V>[]哈希桶,桶下挂单向链表;
  3. Segment继承ReentrantLock,自身就是锁对象。

2. 并发机制

  • 写操作(put/remove):先定位 key 对应 Segment,调用lock()独占锁,操作完成释放;只会锁住一个分段,其他分段并发读写不受影响;
  • 读操作(get)无锁,HashEntry 的valuenextvolatile修饰,保证内存可见性,读到最新值;仅 value 为 null 时加锁重读;
  • 扩容:单段独立扩容,互不干扰。

缺陷

  1. 默认分段 16,并发上限固定;分段越多内存占用越高;
  2. 链表查询效率低,hash 冲突严重时 O (n);
  3. size () 统计总数需要锁住全部 Segment,并发统计开销大。

三、JDK 8 核心实现(主流版本,重点)

3.1 底层数据结构

和 HashMap 结构统一:数组 (Node []) + 单向链表 + 红黑树

  1. 哈希桶数组:transient volatile Node<K,V>[] table,volatile 保证数组可见;
  2. Node:普通链表节点,val、next均为volatile
  3. TreeNode:红黑树节点,冲突链表长度≥8 转红黑树,≤6 退回链表;
  4. ForwardingNode:扩容标记节点,table 扩容时旧桶挂载该节点,线程碰到协助扩容;
  5. ReservationNode:computeIfAbsent 计算占位节点。

3.2 并发核心机制:CAS + synchronized(取消 Segment)

锁粒度从分段缩小到单个哈希桶头节点,并发能力大幅提升:

  1. 无锁 CAS:初始化 table、空桶插入新节点、扩容转移节点,全部用 CAS 乐观锁,无阻塞;
  2. 桶头 synchronized 锁:哈希桶已有数据时,锁住当前桶的首节点,仅阻塞同一个 hash 桶的并发操作,不同桶完全并发;
  3. volatile 关键字:table、节点 val/next、size 计数器保证多线程内存可见。
CAS 关键方法

java

运行

// 比较并替换数组指定下标节点,成功返回true static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) // 获取指定下标节点 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) // 设置指定下标节点 static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)

3.3 核心属性

java

运行

// 哈希表数组,初始化懒加载(第一次put才创建) transient volatile Node<K,V>[] table; // 扩容时临时转移数组,非扩容时为null private transient volatile Node<K,V>[] nextTable; // 计数器控制:高16位记录size,低16位存储扩容阈值/标记 private transient volatile long baseCount; // 扩容阈值,容量*负载因子(默认0.75) private transient int threshold; // 扩容控制标记:负数=扩容中,正数=扩容并行线程数 private transient volatile int sizeCtl;

sizeCtl 状态说明:

  • 0:table 未初始化;
  • 0:初始化 / 扩容阈值;

  • -1:正在初始化;
  • 负数(n<=-2):-(1+扩容线程数),代表多线程协同扩容。

3.4 put () 完整流程(核心源码逻辑)

java

运行

public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { // 不允许key/value为null(HashMap允许key一个null、多个null value) if (key == null || value == null) throw new NullPointerException(); // 1. 计算hash,扰动函数减少冲突 int hash = spread(key.hashCode()); int binCount = 0; for (Node<K,V>[] tab = table;;) { // 自旋循环,直到插入成功 Node<K,V> f; int n, i, fh; // 分支1:table未初始化,CAS初始化数组 if (tab == null || (n = tab.length) == 0) tab = initTable(); // 分支2:当前桶为空,CAS直接新建Node放入桶,成功跳出循环 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) break; } // 分支3:当前桶是ForwardingNode,说明正在扩容,当前线程协助扩容 else if ((fh = f.hash) == MOVED) tab = helpTransfer(tab, f); // 分支4:桶已有数据,锁住桶头节点f,单桶独占锁 else { V oldVal = null; synchronized (f) { // 双重校验:防止锁获取前桶头节点被替换 if (tabAt(tab, i) == f) { // 链表节点 if (fh >= 0) { binCount = 1; // 遍历链表,key存在则覆盖,不存在尾插 for (Node<K,V> e = f;; ++binCount) { K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { pred.next = new Node<K,V>(hash, key, value, null); break; } } } // 红黑树节点 else if (f instanceof TreeBin) { Node<K,V> p; binCount = 2; // 红黑树插入 if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { oldVal = p.val; if (!onlyIfAbsent) p.val = value; } } } } // 链表长度≥8,转为红黑树 if (binCount != 0) { if (binCount >= TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } // 累加元素数量,触发扩容判断 addCount(1L, binCount); return null; }

关键特性:

  1. 禁止 null key /null value:HashMap 允许,ConcurrentHashMap 直接抛 NPE,避免并发下空值歧义;
  2. 懒加载:第一次 put 才初始化 table,节省内存;
  3. 空桶无锁 CAS 插入,性能极高;
  4. 遇到扩容节点主动协助转移,多线程并行扩容;
  5. 仅锁住单个桶头,其他桶并发读写完全不受阻塞。

3.5 get () 无锁读取流程

读操作全程不加锁,依靠volatile可见性保证数据实时:

  1. 计算 hash 定位桶下标;
  2. 获取桶头节点,volatile 读取最新引用;
  3. 链表 / 红黑树遍历匹配 key,直接返回 val;
  4. 若读到 ForwardingNode(扩容中),调用 find 协助读取转移中的数据。

无锁设计让读操作几乎无性能损耗,高并发读场景优势巨大。

3.6 扩容机制(transfer 多线程协同扩容)

  1. 触发条件:addCount计数后元素数量 ≥ threshold(容量 * 0.75);
  2. 扩容容量:原容量 * 2;
  3. 多线程协同:
    • 单线程扩容效率低,其他线程 put/get 时发现ForwardingNode,主动进入helpTransfer分担桶迁移任务;
    • 每个线程分配一段哈希桶迁移,迁移完成标记 ForwardingNode;
  4. 迁移逻辑:每个桶内链表拆分为低位链表(原下标)、高位链表(原下标 + 旧容量),分别挂载到新 table;
  5. 扩容期间读写正常可用,不会阻塞全局。

3.7 size () 元素总数统计

JDK7 需要锁住全部 Segment,性能差; JDK8 采用分段计数器 baseCount + CounterCell 数组(类似 LongAdder):

  1. 低并发:直接 CAS 修改baseCount
  2. 高并发竞争:线程分散写入不同CounterCell
  3. size () = baseCount + 所有 CounterCell 累加和; 无需加全局锁,统计性能大幅提升。

四、核心方法与并发特性对比

1. 读写分离优势

  • 读:无锁,volatile 可见;
  • 写:仅锁单个哈希桶,不同桶并行写入; 远优于 Hashtable 全局锁。

2. 常用方法并发语义

  1. get(key):无阻塞,返回最新可见值;
  2. put(k,v):覆盖旧值,单桶互斥;
  3. putIfAbsent(k,v):key 不存在才插入,原子操作;
  4. remove(k)/remove(k,v):原子删除,锁桶头;
  5. computeIfAbsent(k, func):不存在则执行函数计算 value 插入,全程原子;
  6. compute(k, func):存在则更新,原子操作;
  7. merge(k, val, func):合并更新,原子; 以上复合操作都是原子操作,并发下不会出现数据竞争问题,HashMap 无原子复合方法。

五、和 HashMap、Hashtable 完整对比

表格

特性HashMapHashtableConcurrentHashMap(JDK8)
线程安全不安全安全安全
锁机制无锁方法全量 synchronizedCAS + 桶节点 synchronized
null 键值允许 1 个 null key,任意 null value不允许 null不允许 null key/value
底层结构数组 + 链表 + 红黑树数组 + 链表数组 + 链表 + 红黑树
并发性能高(单线程)极低(并发)极低,全局锁极高,细粒度锁
扩容单线程单线程多线程协同扩容
复合原子操作有但全局锁丰富原子 API
初始容量161116
负载因子0.750.750.75

六、常见面试高频考点

  1. 为什么废弃 Segment 分段锁?Segment 本质是 ReentrantLock,内存开销大;并发上限固定 16;JDK8 锁粒度细化到单个桶,并发度更高、内存占用更小。

  2. 为什么不允许 null key/value?并发场景下,get 返回 null 无法区分:key 不存在 /value 本身是 null;无锁读无法同步判断,容易产生业务逻辑歧义。

  3. 扩容时其他线程读写是否阻塞?不会全局阻塞:读可正常访问旧 table,写线程会协助扩容,扩容完成切换新表,全程可用。

  4. synchronized 锁桶头为什么安全?同一 hash 桶的元素只会竞争同一个头节点锁;不同桶互不干扰,锁粒度极小;插入前双重校验桶头引用,防止 CAS 替换后锁失效。

  5. 并发下 HashMap 死循环,ConcurrentHashMap 为什么不会?HashMap 扩容单线程头插法,并发转移链表形成循环; ConcurrentHashMap 扩容多线程尾拆分链表,且迁移时加桶锁,不会出现环形链表。

  6. size () 为什么不用加全局锁?使用 LongAdder 思想,baseCount+CounterCell 分散计数,牺牲微小精度换取超高并发统计性能;最终求和是近似值(统计过程有新增删除),无法做到精准瞬时总数。

七、使用场景与注意事项

使用场景

  1. 多线程同时读写哈希表,替代 Hashtable、Collections.synchronizedMap(new HashMap())
  2. 高并发缓存、本地消息映射、多线程计数映射。

注意事项

  1. 复合操作优先使用内置原子方法(putIfAbsent/computeIfAbsent),不要if(get()==null) put(),非原子会并发覆盖; 错误示例:

    java

    运行

    // 线程不安全 if (map.get("key") == null) { map.put("key", 1); } // 正确原子写法 map.computeIfAbsent("key", k -> 1);
  2. 迭代器是弱一致性,不是快速失败(fail-fast):遍历期间其他线程新增 / 删除,迭代器不会抛ConcurrentModificationException,可能读到部分新数据;HashMap 迭代器 fail-fast,并发修改直接报错;
  3. 大批量数据读写可合理设置初始容量,减少频繁扩容开销;
  4. 不要存储 null,否则直接 NPE。
http://www.jsqmd.com/news/1189123/

相关文章:

  • 高频版汇率查询接口:今日实时汇率查询、汇率转换与外汇实时行情
  • DrissionPage实战:高效稳定爬取拉勾网招聘数据
  • 一篇讲透电容:从“存电”到滤波、耦合,再到电路板维修
  • 闲置钻戒变现避坑实录:盘点上海回收行业常见压价套路与应对方法 - 讯息早知道
  • 全闪分布式存储:架构、优势与应用实践
  • 【信息科学与工程学】【物理/化学和工程技术】第一百三十二篇 损伤学 系列二01 ——网络中的损伤学02
  • 夏季杭州旧金回收行情解读,全域多点位实体门店当场称重 - 小蝶回收测评
  • ROS2服务通信原理与实战:掌握强一致性RPC机制
  • 【算法基础】博弈论入门:基础博弈论模型与SG函数基础详解(洛谷 P2197 C++代码)
  • AI智能体技术架构与开发实践指南
  • 【云服务器配置越高越好吗?很多人第一步就选错了】
  • 紧急更新!OpenAI 2024 Q2对话API重大变更对语言学习的影响(仅剩72小时适配窗口)
  • 金水区正规奢侈品回收老店,中检持证鉴定,无隐形扣费,24 小时在线估价 - 全城热点
  • 武汉钻石回收套路多?GIA证书避坑清单收好 - 讯息早知道
  • 奢二网武汉上门收钻实测,GIA证书裸钻全程录像不拆镶,无损检测不损伤戒托品相 - 讯息早知道
  • 微信/QQ群自动化管理方案:NLP与规则引擎实践
  • 物联网安全芯片SE050与PIC18F4682的硬件集成方案
  • 2026 深圳训犬寄养乐园实测排名|实地探店伴西西万平生态基地,出差遛狗双刚需一站式解决 - 同城宠物优选基地
  • UE4材质系统实战:Mask打包、球形法线与世界空间色彩控制打造真实植被
  • TB67H480FNG+STM32F103RC步进电机控制方案解析
  • Linux开机自启动:从rc.local到systemd的演进与实战避坑指南
  • 低轨卫星通信载荷高精度供电系统的电源管理技术研究——基于ASP7A84AS抗辐射低压差线性稳压器的应用分析
  • 寄摩托车用什么物流省钱?慧寄侠比价指南 - 快递物流资讯
  • GPU PRO 5 - 4.6 Adaptive Scalable Texture Compression 笔记
  • C语言实现高性能ECS框架:数据驱动架构与缓存优化实战
  • 2026年7月杭州刑事律师推荐:曾智光律师领衔五强服务商核心能力榜单与实战案例解读 - 优企名品
  • HydroViewer-在线水动力模拟可视化解决方案
  • 2026跨境出海新风口:AI GEO生成式引擎优化,破解海外流量内卷困局
  • AI写作论文怎样不越界?从文献整理、初稿生成到降重润色的合规使用边界 - AI论文先行者
  • 支付系统注意事项-工作记录