【多线程】ConcurrenHashMap底层原理

📅 2026/7/24 18:58:54
【多线程】ConcurrenHashMap底层原理
【多线程】ConcurrenHashMap底层原理【一】核心定位【二】JDK8 底层核心结构【三】核心并发机制JDK8 最关键改动核心规则【四】put () 源码流程逐段拆解putVal 关键流程总结【五】get () 读取流程无锁【六】扩容机制 transfer ()【七】JDK7 VS JDK8 核心差异面试高频【八】重点注意事项 生产坑点源码衍生实践问题1. key、value 禁止为 null2. 仅单个方法线程安全**复合操作不具备原子性最大坑**3. 弱一致性无法做到强实时4. size () 统计是估算值5. 扩容开销巨大提前预估容量6. 不要在 synchronized 锁内嵌套 CHM 操作避免死锁风险7. 红黑树转换条件8. 并发下 iterator 不会触发 fail-fast【一】核心定位ConcurrentHashMap线程安全的哈希表替代HashtableHashtable方法加synchronized锁整个数组并发激烈竞争下性能极差HashMap线程不安全并发 put 会出现死循环、数据丢失ConcurrentHashMap细粒度锁高并发读写性能优秀。【二】JDK8 底层核心结构[NodeK,V[] table] 哈希桶数组 ↓ 桶位Node链表 / TreeNode红黑树数组 NodeK,V[] table存放哈希桶延迟初始化第一次 put 才创建Node链表节点val和next使用volatile保证可见性TreeNode红黑树节点链表长度 ≥ 8 转为红黑树元素 ≤6 退化为链表sizeCtl核心控制变量volatile int重中之重// 源码定义 private transient volatile int sizeCtl;sizeCtl状态含义0table 未初始化-1正在初始化-1-(1 扩容线程数)代表正在扩容0初始化阈值 / 扩容阈值容量*0.75【三】核心并发机制JDK8 最关键改动JDK7Segment 分段锁多个独立 HashTable锁粒度是 SegmentJDK8废弃 Segment改为✅CAS synchronized 锁单个桶头节点 (Node)锁粒度细化到哈希桶并发性能大幅提升。核心规则桶为空table [i]null使用CAS尝试写入新 Node无锁竞争桶不为空存在头节点对桶头 Node 加 synchronized锁住当前这一条链表 / 红黑树只会锁住当前 hash 对应的桶其他桶可以并发读写互不阻塞。【四】put () 源码流程逐段拆解publicVput(Kkey,Vvalue){returnputVal(key,value,false);}finalVputVal(Kkey,Vvalue,booleanonlyIfAbsent){// 约束key、value 不能为nullHashMap允许key/value一者nullCHM全都不允许if(keynull||valuenull)thrownewNullPointerException();inthashspread(key.hashCode());// 扰动函数降低hash冲突intbinCount0;for(NodeK,V[]tabtable;;){// 自旋循环NodeK,Vf;intn,i,fh;// 1. table还未初始化先初始化数组if(tabnull||(ntab.length)0)tabinitTable();// 2. 当前桶位 tab[i] nullCAS插入头节点无锁elseif((ftabAt(tab,i(n-1)hash))null){if(casTabAt(tab,i,null,newNodeK,V(hash,key,value,null)))break;// CAS成功跳出循环}// 3. 检测到当前桶是 ForwardingNode代表正在扩容当前线程协助扩容elseif((fhf.hash)MOVED)tabhelpTransfer(tab,f);// 4. 桶不为空上锁执行链表/红黑树写入else{VoldValnull;// 核心synchronized 锁住桶头节点 fsynchronized(f){// 双重校验防止上锁期间头节点被其他线程修改if(tabAt(tab,i)f){// 链表节点if(fh0){binCount1;for(NodeK,Vef;;binCount){Kek;if(e.hashhash((eke.key)key||(ek!nullkey.equals(ek)))){oldVale.val;if(!onlyIfAbsent)e.valvalue;break;}NodeK,Vprede;if((ee.next)null){pred.nextnewNodeK,V(hash,key,value,null);break;}}}// 红黑树节点elseif(finstanceofTreeBin){NodeK,Vp;binCount2;if((p((TreeBinK,V)f).putTreeVal(hash,key,value))!null){oldValp.val;if(!onlyIfAbsent)p.valvalue;}}}}// 5. binCount链表长度达到阈值链表转红黑树 TREEIFY_THRESHOLD8if(binCount!0){if(binCountTREEIFY_THRESHOLD)treeifyBin(tab,i);if(oldVal!null)returnoldVal;break;}}}// 计数判断是否触发扩容addCount(1L,binCount);returnnull;}putVal 关键流程总结不允许 key/value 为 null重要区别于 HashMapspread()扰动函数高低位混合减少哈希碰撞static final int spread(int h) { return (h ^ (h 16)) HASH_BITS; }数组未初始化 →initTable()利用 sizeCtl 竞争初始化只有一个线程初始化其他线程自旋等待目标桶为空 →CAS 新增 Node无锁写入桶是ForwardingNode→ 协助扩容helpTransfer桶存在数据 →synchronized 锁桶头 Node遍历链表key 存在则覆盖不存在尾部追加如果是红黑树执行树节点新增链表长度≥8 →treeifyBin()转为红黑树addCount()更新元素数量检查是否需要扩容【五】get () 读取流程无锁publicVget(Objectkey){NodeK,V[]tab;NodeK,Ve,p;intn,eh;Kek;inthspread(key.hashCode());if((tabtable)!null(ntab.length)0(etabAt(tab,(n-1)h))!null){// 命中头节点if((ehe.hash)h){if((eke.key)key||(ek!nullkey.equals(ek)))returne.val;}// ForwardingNode 扩容状态查询迁移后的新表elseif(eh0)return(pe.find(h,key))!null?p.val:null;// 遍历链表while((ee.next)!null){if(e.hashh((eke.key)key||(ek!nullkey.equals(ek))))returne.val;}}returnnull;}✅get 全程不加锁依靠volatile保证可见性tabAt()使用Unsafe.getObjectVolatile读取桶头保证读到最新数据Node 内部val、next被volatile修饰并发场景下写操作对 volatile 的修改读线程立刻可见存在弱一致性get 可能读到旧数据不保证实时强一致性【六】扩容机制 transfer ()扩容条件元素数量 ≥容量 * 0.75新数组容量 原容量 ×2多线程协助扩容一个线程触发扩容后其他执行 put/get 的线程发现桶是ForwardingNode主动帮忙迁移数据提升迁移速度ForwardingNode标记正在迁移的桶hash MOVED (-1)数据迁移把原桶数据拆分到newTab[i]和newTab[ioldCap]【七】JDK7 VS JDK8 核心差异面试高频表格特性JDK7 ConcurrentHashMapJDK8 ConcurrentHashMap锁模型Segment 分段锁默认 16 段锁粒度大CAS synchronized 锁桶头 Node粒度更小底层结构数组 链表数组 链表 红黑树冲突优化链表链表过长转红黑树查询 O (logn)扩容每个 Segment 独立扩容全局数组扩容支持多线程协助迁移sizeCtl无核心控制初始化、扩容状态【八】重点注意事项 生产坑点源码衍生实践问题1. key、value 禁止为 nullif (key null || value null) throw new NullPointerException();原因get (key) 返回 null无法区分key 不存在 /key 存在 valuenull。HashMap 允许keynullCHM 直接抛 NPE。2. 仅单个方法线程安全复合操作不具备原子性最大坑// ❌ 错误线程不安全 if (map.get(key) null) { map.put(key, value); }get put两次操作中间存在线程间隙并发下会出现覆盖。✅ 正确原子写法map.putIfAbsent(key, value);同类原子 APIcompute()、computeIfAbsent()、computeIfPresent()、merge()原理这些方法内部在 synchronized 桶锁内完成整套逻辑保证原子。3. 弱一致性无法做到强实时get 无锁遍历迭代器也是弱一致性迭代迭代过程中其他线程新增 / 删除元素迭代器不一定感知不会像 HashTable 那样抛出ConcurrentModificationException。 如果业务需要强一致性快照需要额外加锁。4. size () 统计是估算值map.size()不是实时精确值底层通过baseCount CounterCell[]累加并发更新时存在短暂误差。需要精确数量map.mappingCount()同样是估算极高并发场景无法做到绝对精准。5. 扩容开销巨大提前预估容量初始化构造器// initialCapacity 是预估元素数量不是table数组容量 new ConcurrentHashMap(100);内部会计算阈值推荐预估数据量避免频繁扩容扩容会大量拷贝节点CPU 上涨。6. 不要在 synchronized 锁内嵌套 CHM 操作避免死锁风险锁顺序不一致容易产生死锁同时不要在compute/merge内部递归修改同一个 CHM会阻塞。7. 红黑树转换条件链表长度≥8尝试转红黑树treeifyBin会先判断数组容量 64优先选择扩容而不转树红黑树节点数量≤6退化为链表。8. 并发下 iterator 不会触发 fail-fastHashMap 迭代器强一致性并发修改抛ConcurrentModificationExceptionCHM 迭代器弱一致不会报错但可能读到新旧混合数据。