ConcurrentHashMap 源码解析(JDK8)高并发哈希表的终极实现
一、先一句话抓住核心(JDK7 vs JDK8)
要透彻理解JDK8 ConcurrentHashMap的设计精髓,必须从其演进脉络入手。
JDK7版本采用“分段锁”架构:将哈希表划分为多个Segment,每个Segment独立加锁。这种设计虽优于Hashtable的全局锁,但锁粒度仍停留在Segment级别,并发度受限于预设的分段数量。
JDK8的设计哲学发生了根本性变革:数据结构回归经典的数组+链表/红黑树组合;同步机制则升级为CAS无锁操作配合synchronized锁定单个哈希桶(头节点)。锁粒度被压缩至单个桶,实现了并发性能的指数级提升。
核心结论:JDK8摒弃了分段锁,采用极细粒度的桶级别锁与无锁CAS协同,这是为现代高并发场景量身定制的架构。
二、JDK8 ConcurrentHashMap 核心结构
剖析源码,其核心数据结构清晰而高效:
public class ConcurrentHashMap {
// 哈希表数组
transient volatile Node[] table;
// 链表转红黑树阈值
static final int TREEIFY_THRESHOLD = 8;
// 红黑树退化为链表阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 树化的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 控制标识:初始化、扩容、计数
transient volatile int sizeCtl;
}
三、核心 Node 节点(JDK8 真实结构)
底层存储单元Node节点的定义,奠定了高并发读写的基础:
static class Node implements Map.Entry {
final int hash;
final K key;
volatile V val;
volatile Node next;
// ...
}
这里有三个关键设计值得深入分析:
首先,table数组本身由volatile修饰,确保了数组引用本身的可见性,任何线程都能瞬时感知到数组的更新。
其次,Node节点的val和next字段同样被声明为volatile,这为无锁化的读操作提供了内存语义保障。
最后,sizeCtl变量是整个容器的控制中枢。它身兼多职:既是初始化的控制信号,也是扩容的阈值与协调器。深刻理解其状态变迁,就掌握了ConcurrentHashMap并发逻辑的核心。
四、核心流程:put 方法源码(最精简准确版)
理论结合实践,以下精简版putVal流程涵盖了所有核心并发场景:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node[] tab = table;;) {
Node f; int n, i, fh;
// 1. 数组为空 → 初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 2. 对应下标为空 → CAS 无锁插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<>(hash, key, value, null)))
break;
}
// 3. 头节点 hash == MOVED → 正在扩容,帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 正常哈希冲突 → 锁头节点
else {
V oldVal = null;
synchronized (f) { // 只锁当前桶的头节点!
if (tabAt(tab, i) == f) {
if (fh >= 0) {
// 链表插入
} else {
// 红黑树插入
}
}
}
}
}
addCount(1L, binCount);
return oldVal;
}
掌握此流程,足以应对关于put操作的深度提问。其执行逻辑可归纳为四步:
第一步,参数校验。key和value均不允许为null,这是保证并发语义清晰性的铁律。
第二步,哈希计算。通过spread方法对键的哈希码进行二次扰动,优化分布,减少碰撞。
第三步,核心循环。包含四个关键分支:若数组未初始化,则触发初始化;若目标桶为空,则通过CAS无锁插入新节点,这是最高效的路径;若检测到头节点为特殊标记(MOVED),表明正处于扩容阶段,当前线程将协助进行数据迁移;若发生常规哈希冲突,则使用synchronized锁定该桶的头节点,随后在链表或红黑树中完成插入。
第四步,后续处理。插入成功后更新元素计数,并判断是否触发扩容。若链表长度达到阈值8且数组容量不小于64,则将链表转换为红黑树,以保障极端情况下的性能。
五、为什么 JDK8 这么强?
分析核心流程后,其高性能根源可归结为四大设计优势:
锁粒度极致细化。 仅锁定发生冲突的单个哈希桶,其余桶的读写操作完全并行,极大提升了并发吞吐量。
无冲突场景零锁竞争。 多数插入操作目标桶为空,直接通过CAS完成,性能开销接近无锁的HashMap。
多线程协同扩容。 这是JDK8设计的精髓。扩容任务被拆分为多个子区间,所有参与的线程可并行迁移数据,而非被动等待,大幅缩短了扩容导致的停顿时间。
自适应数据结构。 链表与红黑树根据实际情况动态转换。在遭遇严重哈希冲突时,能将操作时间复杂度从O(n)优化至O(log n),有效抵御哈希碰撞攻击,保障了性能下限。
六、必须搞懂的关键细节(面试高频)
宏观设计之外,以下细节是区分“了解”与“精通”的关键。
1. 为什么 key 和 value 不能为 null?
HashMap允许null键值,但ConcurrentHashMap明确禁止。根本原因在于并发语义的清晰性:若允许null值,当get(key)返回null时,调用者无法区分是该key不存在,还是其关联的value本就是null。这种二义性会破坏线程安全契约,因此采用最严格的设计,杜绝null。
2. get 方法真・完全无锁
这是ConcurrentHashMap读性能卓越的基石。get操作全程无需任何锁。其安全性依赖于volatile的内存可见性保证:table数组引用、Node节点的val和next字段均为volatile。只要写操作遵循规则更新这些字段,读线程就能安全地获取最新值。读取桶头节点时,使用tabAt提供的原子读语义。
3. sizeCtl 的 4 种含义(超级高频)
此变量是理解容器并发控制的总开关,其数值代表四种不同状态:
等于-1:表示数组正在初始化中。
小于-1:表示正在进行扩容,其低位数记录了参与扩容的线程数量。
等于0:表示创建时未指定初始容量,采用默认容量。
大于0:此状态最为关键,其值代表下一次触发扩容的阈值(容量 * 负载因子,默认为0.75)。
4. 为什么链表长度 ≥8 才树化?
阈值8的设定基于严谨的概率统计。在理想的哈希函数下,根据泊松分布,单个哈希桶内链表长度达到8的概率极低(约千万分之六)。若实际达到此长度,通常意味着遇到了哈希攻击或hashCode实现质量不佳。此时将链表转为红黑树,旨在将最坏情况下的操作复杂度从O(n)降至O(log n),确保性能底线。
5. 树化必须满足两个条件
链表长度≥8仅是前提之一。另一个必要条件是数组长度必须达到MIN_TREEIFY_CAPACITY(默认64)。若数组容量尚小,优先选择扩容来分散节点,而非直接树化。因为扩容能从根源上缓解冲突,而树化会引入额外的内存与性能开销。
6. 扩容机制(JDK8 灵魂)
扩容机制是ConcurrentHashMap并发设计的集中体现。触发条件是元素数量达到阈值(sizeCtl)。新容量为旧容量的两倍。迁移过程采用“分治”策略:将旧数组划分为多个区间,由多个线程并行迁移各自区间内的桶。迁移过程中,已处理的桶头节点会被替换为ForwardingNode(其hash值为MOVED),其他线程在读写时遇到此标记,会主动加入协助迁移。待所有数据迁移完毕,才会将引用指向新table。整个过程高效、协同,最大化利用了多核资源。
7. size () 是弱一致性
size()方法返回的是近似值,而非实时精确计数。因为CHM并未为了获取绝对准确的数量而全局加锁,那会严重牺牲性能。它是通过汇总各个计数单元的值来估算总量。因此,在高并发更新下,它提供的是一个弱一致性的视图。若业务仅需大致数量,size()完全适用;若强依赖精确计数,则需考虑其他方案。官方也推荐使用mappingCount()方法,其返回long类型,更不易溢出。
七、JDK7 vs JDK8 一张表总结
(此处保留原文结构,内容已整合至第一部分“先一句话抓住核心”)
八、总结(可直接背)
JDK8的ConcurrentHashMap通过一系列精妙的设计,实现了业界领先的并发性能:它摒弃了分段锁,采用数组+链表/红黑树的基础结构;读操作完全无锁,写操作在无冲突时使用CAS,有冲突时仅锁定单个桶;扩容过程支持多线程协同,极大提升了效率。同时,通过禁止null键值保证了语义清晰,通过弱一致性的size()优先保障了性能。
因此,在高并发场景下的选型非常明确:首选ConcurrentHashMap。传统的HashMap与Hashtable因线程安全问题,已不在考虑之列。其设计堪称并发编程领域的典范之作。
