资讯详情

ConcurrentHashMap 底层原理

📅 2026/10/20 2:09:47 | 华诺云谱 👁 阅读
ConcurrentHashMap 底层原理
ConcurrentHashMap 是并发场景下首选的线程安全哈希表用来替代老旧的 HashTable 以及Collections.synchronizedMap。很多开发者只知道它线程安全锁粒度比 HashTable 更细但重点考察JDK7 分段锁设计思想、JDK8 为什么废弃 Segment、CASsynchronized 锁桶头节点原理、多线程协助扩容机制、Node 节点 volatile 修饰的意义、复合操作不具备原子性这个高频踩坑点。本文基于 JDK7 与 JDK8 源码对比从底层结构、核心成员变量、put/get/remove 源码流程、扩容机制、并发安全保障、常见误区、开发踩坑完整讲解吃透并发哈希表。前言HashMap 是非线程安全并发 put 会丢失数据HashTable 所有方法直接加synchronized锁住整个哈希表同一时间只能一个线程读写并发性能极差已经基本淘汰。Collections.synchronizedMap本质也是对所有方法加全局锁和 HashTable 类似并发性能差。ConcurrentHashMap简称 CHM分段锁 / 桶级细粒度锁允许多个线程操作不同哈希桶并发读写性能大幅提升。版本差异巨大JDK7 采用 Segment 分段锁JDK8 重构底层取消 Segment改用CAS synchronized锁桶头节点是面试重点。一、JDK7 ConcurrentHashMap 原理了解即可底层结构SegmentK,V[] segments分段数组每个 Segment 内部维护一张HashEntryK,V[] table哈希表。 Segment 继承ReentrantLockSegment 本身就是一把独立可重入锁。默认 Segment 数量 16代表最大并发度为 16操作 KV 时先定位 key 属于哪一个 Segment只锁住当前 Segment其他 Segment 不受影响不同 Segment 可以并发读写相比 HashTable 全局锁并发能力提升。HashEntry 节点static final class HashEntryK,V { final int hash; final K key; volatile V value; volatile HashEntryK,V next; }value与next用 volatile 修饰保证多线程可见性。JDK7 缺陷Segment 数量一旦初始化无法扩容固定 16 段并发上限固定结构复杂内存开销大不支持多线程协助扩容扩容时只有当前线程负责迁移数据扩容速度慢哈希冲突严重时单个 Segment 内部链表过长查询退化为 O (n)。JDK8 彻底废弃 Segment重构底层结构和 HashMap 保持一致数组 链表 红黑树。二、JDK8 ConcurrentHashMap 核心成员变量 Node 节点// 初始容量162的幂 static final int DEFAULT_CAPACITY 16; // 最大容量 static final int MAXIMUM_CAPACITY 1 30; // 默认负载因子0.75和HashMap一致 static final float LOAD_FACTOR 0.75f; // 链表转红黑树阈值 static final int TREEIFY_THRESHOLD 8; // 红黑树退链表阈值 static final int UNTREEIFY_THRESHOLD 6; // 树化最小数组容量 static final int MIN_TREEIFY_CAPACITY 64; // 协助扩容的线程数量上限 static final int MAX_RESIZERS (1 (31 - RESIZE_STAMP_BITS)) - 1; // 哈希表数组 transient volatile NodeK,V[] table; // 扩容时的新数组迁移完成后赋值给table private transient volatile NodeK,V[] nextTable; // 元素总个数用CAS更新 private transient volatile long baseCount; // 控制扩容、标记桶状态 private transient volatile int sizeCtl;Node 节点源码static class NodeK,V implements Map.EntryK,V { final int hash; final K key; volatile V val; volatile NodeK,V next; Node(int hash, K key, V val, NodeK,V next) { this.hash hash; this.key key; this.val val; this.next next; } }重点val、next使用volatile修饰。 保证多线程间可见性读操作 get 不需要加锁直接读取最新值。特殊节点ForwardingNodehash 为MOVED(-1)标记当前桶已经迁移到新数组代表哈希表正在扩容其他线程碰到这个节点会协助扩容。TreeNode红黑树节点。三、sizeCtl 变量JDK8 核心控制标记sizeCtl是控制哈希表初始化、扩容的核心变量不同值含义负数代表正在初始化或者扩容-1数组正在初始化小于 - 1低 16 位代表参与扩容的线程数量0table 未初始化正数代表扩容阈值容量 * 负载因子和 HashMap threshold 作用一样。四、JDK8 put () 完整流程重点public V put(K key, V value) { return putVal(key, value, false); } final V putVal(K key, V value, boolean onlyIfAbsent) { // 不允许key、value为null if (key null || value null) throw new NullPointerException(); int hash spread(key.hashCode()); int binCount 0; for (NodeK,V[] tab table;;) { NodeK,V f; int n, i, fh; // 1. table为空初始化哈希表CAS修改sizeCtl if (tab null || (n tab.length) 0) tab initTable(); // 2. 根据hash定位桶桶为空CAS尝试插入新节点 else if ((f tabAt(tab, i (n - 1) hash)) null) { if (casTabAt(tab, i, null, new NodeK,V(hash, key, value, null))) break; } // 3. 当前桶是ForwardingNode代表正在扩容当前线程协助扩容 else if ((fh f.hash) MOVED) tab helpTransfer(tab, f); else { // 4. 桶不为空对桶头节点加synchronized锁 V oldVal null; synchronized (f) { // 双重校验防止加锁期间桶头节点被修改 if (tabAt(tab, i) f) { if (fh 0) { // 单向链表 binCount 1; for (NodeK,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; } NodeK,V pred e; if ((e e.next) null) { pred.next new NodeK,V(hash, key, value, null); break; } } } else if (f instanceof TreeNode) { // 红黑树节点新增 binCount treeNodePutVal((TreeNodeK,V)f, tab, hash, key, value); } } } // 链表长度8触发树化判断 if (binCount ! 0) { if (binCount TREEIFY_THRESHOLD) treeifyBin(tab, i); if (oldVal ! null) return oldVal; break; } } } addCount(1L, binCount); // 更新元素计数判断是否触发扩容 return null; }put 流程文字总结校验 key/value 不能为 nulltable 为空则调用initTable()初始化数组计算 hash 定位桶下标桶为空CAS 无锁尝试插入节点CAS 失败自旋重试如果桶标记为 MOVEDForwardingNode当前线程协助扩容桶存在节点锁住桶头节点synchronized(f)双重校验防止桶头变更如果是链表遍历找到相同 key 覆盖 value尾部新增节点如果是红黑树走红黑树新增逻辑链表长度达到阈值触发 treeifyBin 树化判断addCount更新元素总数判断是否触发扩容。锁粒度只锁住单个哈希桶的头节点。不同桶可以并发写入并发度远高于全局锁。五、get 查询流程无锁读取public V get(Object key) { NodeK,V[] tab; NodeK,V e, p; int n, eh; K ek; int h spread(key.hashCode()); if ((tab table) ! null (n tab.length) 0 (e tabAt(tab, (n - 1) h)) ! null) { if ((eh e.hash) h) { if ((ek e.key) key || (ek ! null key.equals(ek))) return e.val; } else if (eh 0) return (p e.find(h, key)) ! null ? p.val : null; while ((e e.next) ! null) { if (e.hash h ((ek e.key) key || (ek ! null key.equals(ek)))) return e.val; } } return null; }get 全程不加锁。依靠 Node 中volatile val、next保证可见性线程可以读到最新数据。弱一致性get 无法保证实时最新快照在读的同时其他线程在修改读到的是修改前的旧值。适合读多场景不适合强一致性业务。六、多线程协助扩容 transferJDK8 核心亮点当元素数量超过阈值触发扩容新数组nextTable容量是原数组 2 倍。触发扩容的线程负责迁移桶数据其他线程执行 put/get 操作碰到ForwardingNode(MOVED)主动协助一起迁移数据多个线程分工迁移不同哈希桶加速扩容所有桶迁移完成后将nextTable赋值给 tablenextTable 置空。这是 JDK8 巨大优化扩容不再是单线程任务多线程分担迁移压力扩容耗时降低。七、JDK7 VS JDK8 ConcurrentHashMap 全面对比对比项JDK7 ConcurrentHashMapJDK8 ConcurrentHashMap底层结构Segment 数组 HashEntry 单向链表Node 数组 链表 红黑树锁实现Segment 继承 ReentrantLock分段锁CAS synchronized 锁单个桶头节点锁粒度分段锁锁一段哈希表桶级锁只锁当前链表 / 红黑树根节点并发扩容不支持多线程协助扩容支持多线程协助迁移数据红黑树无链表≥8 且数组≥64 转为红黑树≤6 退链表key/valuenull不允许 key/value 为 null不允许 key/value 为 nullsize 统计分段计数累加baseCount 计数器数组CAS 更新八、为什么 ConcurrentHashMap 不允许 key、value 为 null高频HashMap 允许一个 null keyvalue 可以多个 null。 CHM 不允许原因 调用map.get(key)返回 null 时无法区分两种情况key 不存在key 存在value 本身就是 null。在并发场景下你无法通过containsKey(key)来辅助判断因为get和containsKey两个操作之间其他线程可能修改 map判断失效。 HashMap 是单线程场景使用开发者可以自己控制ConcurrentHashMap 面向并发为了消除歧义直接禁止 null。九、最容易踩的大坑单个方法线程安全复合操作不具备原子性重点CHM 只能保证单独方法是线程安全多个方法组合在一起不是原子操作并发会出现竞态条件。示例先判断 key 不存在再 put。// 并发下不安全 if(!chm.containsKey(key)){ chm.put(key,value); }并发场景线程 A 执行完 containsKey还没有 put线程 B 同时也执行 containsKey两个线程都认为 key 不存在先后执行 put后 put 覆盖前面的值数据丢失。解决方案1.使用 CHM 自带原子方法putIfAbsent(key,value);推荐底层 CAS 锁保证原子chm.putIfAbsent(key,value);2.或者外部手动加锁。同类复合操作get put、get remove都不是原子操作。十、size () 元素计数原理CHM 的元素总数不是简单一个变量。采用baseCountCounterCell[]分段计数竞争低的时候CAS 更新 baseCount并发竞争激烈时线程分散到不同 CounterCell 计数减少 CAS 竞争 size () baseCount 累加所有 CounterCell 的值。好处高并发计数减少 CAS 失败自旋提升性能。十一、开发实战踩坑清单误以为 CHM 全场景线程安全复合操作containsKeyput非原子要用putIfAbsent、computeIfAbsent等原子 API。key/value 不能传入 null直接抛出空指针。get 是弱一致性读取时可能读到正在迁移 / 修改的旧数据不适合强一致性业务。扩容时大量桶迁移CPU 开销上升预估容量初始化减少扩容次数。迭代遍历 CHM遍历过程中集合修改不会抛出ConcurrentModificationException使用弱一致性迭代器。HashMap 迭代器是快速失败 fail-fastCHM 迭代器是弱一致 weakly consistent。十二、常用 API 介绍方法说明是否原子put(k,v)新增 / 覆盖键值对原子get(k)获取 value原子弱一致性remove(k)删除 key原子putIfAbsent(k,v)key 不存在才 put原子computeIfAbsent(k,func)不存在则执行函数生成 value 存入原子containsKey(k)判断 key 是否存在原子size()获取元素总数原子但只是近似值十三、问答Q1JDK8 为什么废弃 JDK7 的 Segment 分段锁ASegment 结构笨重分段数量固定内存开销大。改用 CAS synchronized 锁桶头节点锁粒度更细支持多线程协助扩容底层复用 HashMap 的链表红黑树结构代码更精简并发性能更好。Q2ConcurrentHashMap get 为什么不用加锁ANode 内部 val、next 使用 volatile 修饰保证多线程可见性。读操作可以直接读取最新值无需加锁。但是是弱一致性并发修改时可能读到旧值。Q3CHM 为什么不允许 key 和 value 为 nullHashMap 却可以A并发场景下 get 返回 null无法区分 key 不存在还是 value 等于 null。HashMap 单线程使用可以配合 containsKey 判断CHM 并发下 containsKey 和 get 之间数据会被其他线程篡改无法区分直接禁止 null。Q4CHM 复合操作为什么不安全如何解决AcontainsKey put 是两次独立方法中间可以被其他线程打断不是原子。优先使用putIfAbsent、computeIfAbsent等内置原子方法。Q5CHM 迭代器是快速失败吗A不是。CHM 迭代器是弱一致性迭代器遍历期间其他线程修改集合不会抛出 ConcurrentModificationException能遍历到迭代创建时的快照附近数据。HashMap 迭代器是 fail-fast 快速失败。Q6JDK8 中 synchronized 锁对象是什么A哈希桶的头节点 Node 对象。只有同一个桶的线程才竞争同一把锁不同桶可以并发写入。结语ConcurrentHashMap 是 Java 并发集合的核心。学习重点要理解锁粒度的进化HashTable 全局锁 → JDK7 分段锁 → JDK8 桶级锁。 很多人容易踩坑误以为 CHM 所有场景都线程安全忽略复合操作非原子这个核心考点。开发并发业务优先使用它内置的原子方法避免自己拼接多个方法造成并发 bug。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑