来看看对《ConcurrentHashMap》总结
ConcurrentHashMap 核心原理与源码深度解析
在之前我们学过了HashTable,我们知道它是线性安全的,看他的源代码我们能够发现它是采用synchronized同步锁锁住了整个put方法,get 方法不加锁,但是HashTable 内部几乎所有写方法比如:(put/get/size/clear)都加了 synchronized,这意味着,在同一时间,整个 HashTable 只能有一个线程操作,哪怕你只是读两个不同 key,也要排队。在高并发下直接变成串行执行,性能极差!

因此在jdk1.8之后,我们大多选择采用ConcurrentHashMap ,相对于HashTable 锁太粗暴,ConcurrentHashMap 又安全又快,现代并发场景几乎全面碾压。ConcurrentHashMap采用 CAS + 桶级细粒度锁,读无锁,并发能力极强。
接下来我们来以基于 JDK1.8 版本,从数据结构、putVal 核心插入流程、get 核心查询流程三个维度,拆解ConcurrentHashMap其底层实现与并发安全机制。
一、核心数据结构(JDK1.8 演进核心)
JDK1.8 彻底摒弃了 JDK1.7 的 Segment 分段锁,采用 数组 + 链表 + 红黑树 的三层结构,锁粒度从 Segment 级细化到哈希桶(Node 节点)级,并发吞吐量大幅提升。
1.ConcurrentHashMap的核心组成:

2.核心控制字段
sizeCtl:控制标识,-1 表示正在初始化,-N 表示有 N 个线程正在扩容,≥0 表示阈值或容量;
baseCount:元素个数基准值,配合 CounterCell 实现高效计数;
nextTable:扩容时的新数组,仅在扩容期间非空。




1.3 结构转换规则(同HashMap)
哈希冲突时,先以链表形式存储;
链表长度≥8 且数组长度≥64:链表转为红黑树(降低查询复杂度从 O (n) 到 O (log n));
链表长度 < 8:红黑树转回链表(扩容时触发);
数组长度 < 64:优先扩容数组,不树化(避免小容量下的树化开销)。
二、putVal () 方法核心源码解读
2.1 put 方法最终调用 putVal 内部方法,是 ConcurrentHashMap 并发插入的核心,全程遵循 CAS 无锁 + synchronized 细粒度锁 原则,核心流程共 7 步。

详细解读代码每一步骤:
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 1. 空值校验:key/value 均不允许为 null(与 HashMap 一致)
if (key == null || value == null) throw new NullPointerException();
// 2. 哈希扰动:减少哈希冲突,提升分布均匀性
int hash = spread(key.hashCode());
int binCount = 0; // 记录桶内节点数,用于树化判断
// 3. 自旋:保证插入成功(失败则重试)
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 4. 数组初始化:首次 put 时触发,CAS 保证线程安全
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 5. 空桶 CAS 插入:无锁操作,竞争小则直接插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// !!!去除上面计算出的下标位置上的所在的元素,判断是否为空
// 如果不为空说明当前下标位置镁元素,接下来进行CAS 判断
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // 插入成功,跳出自旋
}
// 6. 扩容协助:当前桶正在扩容,参与数据迁移
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 7. 哈希冲突:加锁桶头节点,处理链表/红黑树插入
else {
V oldVal = null;
synchronized (f) { // 仅锁当前桶头,锁粒度极细
if (tabAt(tab, i) == f) { // 双重检查:避免并发修改导致失效
// 7.1 链表处理:遍历插入/覆盖
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
// key 已存在:覆盖旧值(onlyIfAbsent 控制)
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;
}
}
}
// 7.2 红黑树处理:树节点插入/覆盖
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;
}
}
}
// 9. 计数更新:累加元素个数,检查扩容
addCount(1L, binCount);
return null;
}
具体 CAS方法 如何进行判断和插入,底层有点复杂,本章暂且先不探讨。
2.2 具体关键步骤拆解
步骤 1:空值与哈希预处理
- 禁止 key/value 为 null(与 Hashtable 一致,与 HashMap 不同),直接抛出 NPE;
- 调用 spread () 扰动 hash 值(高位参与运算),减少哈希冲突,保证分布均匀。
步骤 2:数组初始化(initTable)
- 首次 put 时 table 为空,通过 CAS 初始化数组,避免多线程重复初始化;
- 核心逻辑:通过 sizeCtl 控制初始化状态,CAS 保证只有一个线程完成初始化。
步骤 3:空桶 CAS 插入
// !!!去除上面计算出的下标位置上的所在的元素,判断是否为空
// 如果不为空说明当前下标位置镁元素,接下来进行CAS 判断
- 计算桶索引
i = (n - 1) & hash,通过 tabAt () 获取桶头节点; - 桶为空时,调用 casTabAt () 无锁插入,竞争小则直接成功,无需加锁。
步骤 4:扩容协助(helpTransfer)
- 桶头节点 hash 为 MOVED(-1),表示当前桶正在迁移数据;
- 当前线程暂停插入,调用 helpTransfer () 参与数据迁移,提升扩容效率。
步骤 5:哈希冲突加锁处理
- 桶非空且未扩容,对桶头节点 f 加 synchronized 锁(仅锁当前桶,不影响其他桶);
- 双重检查
tabAt(tab, i) == f:避免并发修改导致桶头失效,保证线程安全。
步骤 6:链表 / 红黑树插入
- 链表处理:遍历链表,key 存在则覆盖(onlyIfAbsent 控制),不存在则尾部插入;
- 红黑树处理:调用 TreeBin.putTreeVal (),逻辑与红黑树插入一致,保证 O (log n) 效率。
步骤 7:树化与计数
- 链表长度≥8 且数组长度≥64,调用 treeifyBin () 转红黑树;数组长度 < 64 时优先扩容;
- 调用 addCount () 累加元素个数,同时检查是否触发扩容。
三、get () 方法核心源码解读
3.1get 方法是 ConcurrentHashMap 高并发读取的核心,全程无锁,通过 volatile 保证可见性,查询效率与 HashMap 接近。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 1. 哈希扰动:与 put 保持一致
int h = spread(key.hashCode());
// 2. 快速校验:数组非空、长度有效、桶非空
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 3. 桶头节点匹配:直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 4. 特殊节点处理:红黑树/扩容中转节点
else if (eh < 0) {
// 红黑树:调用 find 方法
// 扩容节点:从新表 nextTable 查询
return (p = e.find(h, key)) != null ? p.val : null;
}
// 5. 链表遍历:逐个匹配 key
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
// 未找到:返回 null
return null;
}
3.2 关键步骤拆解
步骤 1:哈希与快速校验
- 计算哈希值 h,与 put 流程一致,保证定位准确;
- 快速校验 table 非空、长度 n>0、目标桶
(n-1)&h非空,避免无效查询。
步骤 2:桶头节点匹配
- 桶头节点的 hash、key 与目标完全一致,直接返回 val(volatile 保证最新值)。
步骤 3:特殊节点查询
- eh<0 表示当前节点是红黑树(TreeBin)或扩容中转节点(ForwardingNode);
- 红黑树:调用 e.find (),O (log n) 效率查询;
- 扩容节点:从新表 nextTable 查询,保证扩容期间查询结果准确。
步骤 4:链表遍历
- 遍历链表,逐个匹配 hash 与 key,找到则返回 val,未找到则返回 null。
3.3无锁安全核心
- volatile 保证可见性:table、Node.val 均为 volatile 修饰,get 能立即获取其他线程的修改结果;
- 无锁设计:全程不加锁,避免锁竞争带来的性能开销,读性能远优于 Hashtable;
- 一致性保证:遍历过程中,即使有并发修改(如插入、删除),也能基于当前桶的状态查询,不会抛出 ConcurrentModificationExceptionOracle。
四、核心总结与对比
4.1核心优势

4.2 适用场景
- 高并发读写场景:如缓存、配置中心、多线程数据聚合;
- 读多写少场景:无锁读保证高性能,写操作通过细粒度锁保证安全;
- 对性能要求高的哈希集合替代方案:优于 Hashtable,避免 HashMap 的线程安全风险。
4.3 避坑要点
- 禁止 key/value 为 null,否则抛出 NPE;
- 树化阈值为 8,数组长度≥64 才会树化,小容量场景优先扩容;
- 聚合方法(size、isEmpty、containsValue)结果可能不准确,仅适用于监控,不适合业务逻辑控制Oracle;
- 迭代器弱一致:遍历过程中允许修改,不会抛 ConcurrentModificationException,但不保证遍历到最新修改的所有元素Oracle。
五、总结
ConcurrentHashMap 是 JDK1.8 并发集合的核心实现,通过 数组 + 链表 + 红黑树 的结构升级、CAS 无锁 + synchronized 细粒度锁 的并发控制、多线程协助扩容 的性能优化,实现了线程安全与高性能的平衡。putVal 方法的核心是 “空桶 CAS、冲突加锁、扩容协助”,get 方法的核心是 “无锁查询、volatile 可见性”,这两大流程共同支撑了 ConcurrentHashMap 在高并发场景下的稳定运行。
6.扩展
什么是CAS?

更多推荐


所有评论(0)