本文将对 ConcurrentHashMap(CHM)在 Java 8 及更高版本中的底层实现进行一次全景式深度剖析,涵盖其与旧版本的架构演进、五种核心节点类型的设计、桶容量与扩容机制,并通过核心方法源码与逻辑图,助你彻底掌握这一高并发场景下的关键数据结构。
一、为何选择 ConcurrentHashMap?四大 Map 并发能力对比
在高并发编程中,选择合适的 Map 容器至关重要。下表清晰地展示了主流 Map 实现的线程安全特性与适用场景:
| 实现类 |
线程安全 |
锁机制 |
并发瓶颈 |
适用场景 |
| HashMap |
❌ |
无 |
多线程下数据错乱/死循环(Java 7 扩容) |
单线程环境 |
| Hashtable |
✅ |
全表 synchronized |
每次操作锁整个 Map |
遗留系统 |
| Collections.synchronizedMap |
✅ |
外部包装锁整个 Map |
同 Hashtable |
简单同步需求 |
| ConcurrentHashMap (Java 8+) |
✅ |
CAS + synchronized(桶) |
锁单个链表/树头节点 |
高并发核心场景 |
// 错误示范:HashMap 在并发下可能死循环(Java 7)或数据丢失
Map<String, Integer> unsafeMap = new HashMap<>();
// 正确选择:高并发场景首选
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();
关键洞察:ConcurrentHashMap 通过锁粒度极致细化(从 Segment → 单桶)+ 无锁化设计(get 操作)+ 智能扩容,实现了高吞吐与低延迟的完美平衡。
二、Java 7 vs Java 8:架构的革命性演进
Java 8 对 ConcurrentHashMap 进行了彻底的重构,使其设计更加简洁高效。
| 特性 |
Java 7 (Segment 分段锁) |
Java 8+ (CAS + synchronized) |
| 核心结构 |
Segment 数组 + HashEntry 数组 |
Node 数组 + 链表/红黑树 |
| 锁粒度 |
段级锁(默认 16 段) |
桶级锁(锁单个链表头/树根) |
| 扩容机制 |
单线程扩容 |
多线程协同扩容 |
| 数据结构 |
仅链表 |
链表 → 红黑树(优化长链查询) |
| 计数优化 |
无 |
CounterCell 分散计数压力 |
本文聚焦 Java 8+ 的实现(当前生产环境的绝对主流),它彻底摆脱了 Segment 的历史包袱,设计更为现代化。
三、节点类型全景解析:职责与协作机制
ConcurrentHashMap 的桶(table 数组元素)可存储 5 种核心节点类型,每种都承担着特定的职责,这是理解其并发控制的关键。
| 节点类型 |
hash 值 |
使用场景 |
核心作用 |
关键特性 |
| Node |
≥ 0 |
普通链表节点 |
存储键值对(链表结构) |
volatile next 保证可见性;默认结构 |
| TreeNode |
≥ 0 |
红黑树节点 |
树节点数据载体 |
不直接存于桶中,由 TreeBin 管理;继承 Node |
| TreeBin |
-2 |
桶头节点(树根代理) |
管理红黑树 + 读写锁 |
桶中实际存储的是 TreeBin;内部含 root/first 等指针;写操作加锁 |
| ForwardingNode |
-1 |
扩容迁移中 |
占位 + 指向新表 |
nextTable 指向新数组;触发 helpTransfer 协助扩容 |
| ReservationNode |
-3 |
computeIfAbsent 执行中 |
临时占位防并发插入 |
仅用于 computeIfAbsent;计算完成后替换为正常节点 |
节点关系与使用流程
桶位置 (table[i])
│
├─ 情况1: null → CAS 插入 Node(链表头)
│
├─ 情况2: Node(链表)
│ ├─ 长度 < 8 → 继续链表插入
│ └─ 长度 ≥ 8 且 table.length ≥ 64 → treeifyBin()
│ └─ 桶替换为 TreeBin(内部构建 TreeNode 红黑树)
│
├─ 情况3: TreeBin (hash=-2)
│ └─ 所有树操作通过 TreeBin 代理(含读写锁控制)
│
├─ 情况4: ForwardingNode (hash=-1)
│ └─ 扩容中:put/get 线程调用 `helpTransfer()` 协助迁移
│
└─ 情况5: ReservationNode (hash=-3)
└─ computeIfAbsent 执行中:其他线程等待或重试
关键澄清
- TreeNode ≠ 桶中直接存储:桶中存的是
TreeBin(代理节点),TreeBin 内部维护 TreeNode 组成的红黑树。
- 为什么需要 TreeBin 代理? 避免树操作时锁整个桶影响性能。
TreeBin 内置读写锁机制,写操作加锁,读操作通过状态机实现无锁访问,这与经典的算法/数据结构中的高效设计思想一脉相承。
- ReservationNode 生命周期极短:仅在
computeIfAbsent 计算 value 期间存在,计算完成后立即替换为 Node/TreeBin。
四、桶的数量设计:容量、扩容与负载因子
这是决定 ConcurrentHashMap 性能的基石设计。
| 项目 |
值/规则 |
说明 |
| 初始容量 |
构造参数(默认 16) |
实际初始化时调整为 ≥指定值的最小 2 次幂 |
| 负载因子 |
固定 0.75 |
不可修改(与 HashMap 不同) |
| 扩容阈值 |
sizeCtl = (n << 1) - (n >>> 1) |
即 n * 0.75(n 为当前容量) |
| 扩容倍数 |
2 倍 |
新容量 = 旧容量 << 1 |
| 最小树化容量 |
64 |
链表转树需同时满足:长度 ≥ 8 且 table.length ≥ 64 |
| 最小迁移步长 |
16 |
单线程至少负责 16 个桶的迁移 |
哈希计算与桶定位
- STEP 1:获取原始哈希值
int h = key.hashCode();
- STEP 2:扰动 + 符号位处理
int hash = spread(h);(核心!)
- STEP 3:计算桶索引
int i = (n - 1) & hash;(n = table.length,位运算取模)
static final int HASH_BITS = 0x7fffffff; // 二进制: 01111111 11111111 11111111 11111111
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
扩容触发逻辑
addCount 更新元素总数
↓
是否超过 sizeCtl 阈值? → 否 → 结束
↓ 是
是否有线程正在扩容? → 是 → 尝试协助扩容(helpTransfer)
↓ 否
CAS 将 sizeCtl 设为 -(1 + rs) 【标记扩容开始】
↓
transfer() 扩容主流程:
1. 创建新数组 nextTable(容量×2)
2. 设置 transferIndex = table.length(从后向前分配任务)
3. 每个线程通过 CAS 领取迁移区间 [bound, i)
4. 遍历负责的桶:
- 空桶 → CAS 设为 ForwardingNode
- 非空桶 → synchronized 锁住头节点 → 迁移数据 → 设为 ForwardingNode
5. 所有桶迁移完成 → CAS 替换 table = nextTable → 重置 sizeCtl
容量计算示例
// 构造时指定初始容量 100
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(100);
// 实际初始化容量 = 128(≥100 的最小 2 次幂)
// 扩容阈值 = 128 * 0.75 = 96
// 当元素数 > 96 时触发扩容 → 新容量 = 256
五、Java 8 ConcurrentHashMap 核心机制深度剖析
1. 数据结构全景图
table (Node数组)
│
├─ 桶0: null
├─ 桶1: Node → Node → Node (链表,长度<8)
├─ 桶2: TreeBin (红黑树根) → TreeNode...
├─ 桶3: ForwardingNode (hash=-1,扩容中占位)
└─ 桶4: ReservationNode (computeIfAbsent 占位)
- 关键约束:不支持 null 键/值(避免并发下的歧义,是与
HashMap 的关键区别之一)。
2. 核心字段精解
// 懒初始化的主数组
transient volatile Node<K,V>[] table;
// 扩容时的新数组
private transient volatile Node<K,V>[] nextTable;
// 神奇的控制字段(一值多义!)
private transient volatile int sizeCtl;
// 计数优化:baseCount + CounterCell 数组
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;
sizeCtl 的玄机:
> 0:初始化容量 或 扩容阈值(如 12 = 16*0.75)
-1:正在初始化
-(1 + n):n 个线程正在扩容(如 -3 表示 2 个线程扩容中)
3. 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<K,V>[] tab = table;;) {
Node<K,V> 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. 遇到 ForwardingNode:协助扩容(多线程协作精髓)
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 4. 桶非空:synchronized 锁住头节点(仅锁当前桶!)
else {
V oldVal = null;
synchronized (f) { // 锁粒度:单个桶
if (tabAt(tab, i) == f) { // 双重检查防 ABA
if (fh >= 0) { // 链表
// ...遍历插入/更新
binCount = 链表长度;
} else if (f instanceof TreeBin) { // 红黑树
// ...树插入
binCount = 2;
}
}
}
// 5. 链表转树判断(需同时满足:长度≥8 且 数组长度≥64)
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null) return oldVal;
break;
}
}
addCount(1L, binCount); // 计数+可能触发扩容
return null;
}
设计精髓:
- 无锁优先:空桶使用 CAS,避免加锁开销。
- 锁最小化:
synchronized 仅作用于当前桶头节点,这是实现Java高并发性能的关键。
- 扩容协作:业务线程遇到
ForwardingNode 主动协助迁移。
- 双重检查:
synchronized 内再次校验头节点,防止并发替换。
4. get:完全无锁的高性能读取
源码核心解析
public V get(Object key) {
Node<K,V>[] tab; Node<K,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) { // tabAt 用 Unsafe.getObjectVolatile
if ((eh = e.hash) == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val; // val 为 volatile,保证可见性
else if (eh < 0) // 树节点或 ForwardingNode
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;
}
- 无锁根基:
Node 的 val 和 next 均为 volatile,利用 happens-before 原则保证写操作对后续读可见。
- 零同步开销:高并发读场景性能远超
Hashtable / synchronizedMap。
5. 扩容机制:多线程协同迁移(transfer)
核心逻辑简述(极度简化版)
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
// 计算每个线程负责的桶区间(最小16)
stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;
if (stride < MIN_TRANSFER_STRIDE) stride = MIN_TRANSFER_STRIDE;
// 首次触发扩容的线程创建新数组
if (nextTab == null) {
nextTab = new Node<?,?>[n << 1];
nextTable = nextTab;
transferIndex = n; // 从后向前分配迁移任务
}
// 主循环:每个线程通过 CAS 领取迁移区间 [bound, i)
// 对每个桶:
// 1. 空桶 -> CAS 设为 ForwardingNode
// 2. 非空桶 -> synchronized(桶头) -> 迁移数据 -> 设为 ForwardingNode
// 所有桶迁移完成后,CAS 替换 table 引用,重置 sizeCtl。
}
扩容革命:
- 任务分片:每个线程通过 CAS 领取独立迁移区间。
- 业务线程参与:
put/get 遇到 ForwardingNode 自动调用 helpTransfer 协助,充分利用系统资源。
- 平滑迁移:避免 Stop-The-World,系统吞吐更稳定。
6. 计数优化:CounterCell 解决高并发计数瓶颈(addCount)
核心思想:借鉴 LongAdder,采用 baseCount + CounterCell[] 的分段计数方式。
- 优先尝试 CAS 更新
baseCount(低竞争时高效)。
- 若失败,则获取当前线程对应的
CounterCell,CAS 更新其 value。
- 调用
size() 时,结果为 baseCount 与所有 CounterCell.value 之和。
这种设计在高并发写入场景下,将单一 baseCount 变量的 CAS 竞争压力分散到多个 CounterCell 上,性能提升显著。
六、实战代码验证
高并发计数验证(10线程 × 1000次)
public class CHMDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
int threads = 10;
CountDownLatch latch = new CountDownLatch(threads);
for (int i = 0; i < threads; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
// 原子累加
map.compute("counter", (k, v) -> v == null ? 1L : v + 1);
}
latch.countDown();
}).start();
}
latch.await();
System.out.println("Expected: " + (threads * 1000)); // 10000
System.out.println("Actual: " + map.get("counter")); // 稳定输出 10000
}
}
✅ 运行结果:多次执行均精准输出 10000,验证线程安全性。
⚠️ 对比实验:将 ConcurrentHashMap 替换为 HashMap,结果通常 < 10000(数据丢失)。
七、设计哲学与最佳实践
核心设计思想
- 锁粒度极致细化:从
Segment → 单桶锁,最大化并发度。
- 无锁化优先:
get 完全无锁,空桶 CAS 插入。
- 协作式扩容:业务线程参与迁移,资源利用率最大化。
- 热点分散:
CounterCell 解决计数竞争。
- 结构自适应:链表 → 红黑树动态转换,应对哈希冲突。
使用建议与陷阱
| 场景 |
建议 |
原因 |
| 高并发读多写少 |
✅ 首选 |
无锁 get + 桶级锁写 |
| 需要 null 键/值 |
❌ 改用其他 Map |
CHM 强制非空 |
| 强一致性遍历 |
⚠️ 谨慎 |
迭代器是弱一致性的 |
| 初始容量预估 |
✅ 构造时指定 |
避免频繁扩容 |
| 复合操作 |
✅ 用 compute/merge |
避免 “check-then-act” 竞态条件 |
八、高频面试题速览
-
Q: 为什么不允许 null 键/值?
A: 并发下,get(key) 返回 null 无法区分“key 不存在”还是“value 为 null”,易引发歧义。
-
Q: 链表何时转红黑树?
A: 需同时满足两个条件:链表长度 ≥ 8 且 当前 table 数组长度 ≥ 64。
-
Q: size() 方法如何保证在高并发下的性能与准确性?
A: 通过 baseCount + CounterCell[] 的分段计数机制,近似于 LongAdder 的实现,牺牲了绝对的实时准确性(弱一致性)来换取高并发下的高性能。
-
Q: 扩容时如何保证线程安全?
A: 通过 ForwardingNode 标记正在迁移的桶,配合 synchronized 锁住原桶头节点进行数据迁移,其他线程遇到 ForwardingNode 会协助迁移或在新表中查找。
掌握 ConcurrentHashMap 不仅是掌握一个容器,更是理解高并发系统设计的核心范式。它的设计融合了Java内存模型、锁优化、数据结构等多方面知识,是每位后端 & 架构开发者都应深入研究的经典之作。希望这篇在云栈社区的深度解析能为你点亮一盏明灯。