在 Java 并发编程中,
ConcurrentHashMap是当之无愧的“明星类”。 它既不像HashMap那样“裸奔”,也不像Hashtable那样“一锁锁全表”。 它通过精巧的设计,在线程安全与高性能之间找到了完美的平衡。 本文将带你深入源码,彻底搞懂它的底层实现、扩容机制、锁的使用方式,并对比 JDK 1.7 与 JDK 1.8 的演进与差异。
ConcurrentHashMap 的设计目标非常明确:
✅ 线程安全:多线程读写不出现数据错乱 ✅ 高并发性能:支持多个线程同时读写,互不阻塞 ✅ 可伸缩性:在多核 CPU 下性能随核心数线性增长
为了实现这些目标,它在不同 JDK 版本中采用了完全不同的策略。
JDK 1.7 的 ConcurrentHashMap 采用了 “分段锁”(Segment) 的设计思想:
Segment(默认 16 个)Segment 是一个独立的 ReentrantLock,内部是一个 HashEntry 数组Segment 管理一部分 key,互不干扰ConcurrentHashMap map = new ConcurrentHashMap();
// 默认 concurrencyLevel = 16ConcurrentHashMap
├── Segment[0] → HashEntry[...](锁0)
├── Segment[1] → HashEntry[...](锁1)
├── ...
└── Segment[15] → HashEntry[...](锁15)一个线程访问
Segment[0],另一个线程可以同时访问Segment[1],实现并发写。
Segment 继承自 ReentrantLock,可以独立加锁。Segment,然后调用 lock()。volatile 保证可见性,无需加锁。final Segment<K,V> s = segmentForHash(hash);
s.lock();
try {
// 执行 put/remove
} finally {
s.unlock();
}✅ 优点:并发度高(默认支持 16 个线程并发写) ❌ 缺点:结构复杂,
Segment数量固定,不够灵活
Segment 内部维护自己的 HashEntry 数组。Segment 的元素过多时,只扩容它自己,不影响其他 Segment。缺点:扩容粒度小,但无法利用多核并行优势。
JDK 1.8 彻底重构了 ConcurrentHashMap,放弃了 Segment,回归数组 + 链表/红黑树结构,借鉴了 HashMap 的优化思路,但加入了并发控制。
transient volatile Node<K,V>[] table;table 是一个 Node 数组,每个 Node 是链表节点。table.length >= 64 时,链表转为红黑树(TreeNode)。volatile 保证数组的可见性。table[0] → Node → Node → ...(可能转为 TreeNode)
table[1] → Node
...
table[n-1] → null✅ 优点:结构简单,与
HashMap一致,易于维护和优化。
这是 JDK 1.8 最大的变化之一:用 synchronized 替代 ReentrantLock,锁粒度更小。
synchronized 锁。Node<K,V> f = tabAt(tab, i = (n - 1) & hash); // 定位桶
synchronized (f) { // 只锁这个桶的头节点
if (tabAt(tab, i) == f) {
// 插入或更新
}
}synchronized 经过优化(偏向锁、轻量级锁、自旋),性能已不输 ReentrantLock。synchronized 更轻量,JVM 层面支持,代码更简洁。✅ 优势:锁粒度极小,并发性能极高。
CAS(Compare and Swap)是 JDK 1.8 中实现无锁化操作的核心。
场景 | CAS 作用 |
|---|---|
初始化 table | 只有一个线程能成功 CAS 设置 sizeCtl,避免重复初始化 |
插入第一个节点 | 若桶为空,用 CAS 插入,避免竞争 |
修改控制变量 | 如 sizeCtl、transferIndex 等 |
// 尝试初始化 table
if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 成功的线程进入初始化
}✅ 优势:无锁,性能高,适合低竞争场景。
这是 JDK 1.8 最精妙的设计之一:扩容不再是一个线程的苦力活,而是多线程协作完成。
put 操作后,检查 size 是否超过阈值(0.75 * capacity)tryPresize 或 helpTransfer 触发扩容private transient volatile int sizeCtl;
private transient volatile int transferIndex; // 下一个迁移任务的起始位置sizeCtl 设为 -1,表示“正在初始化”newCap = oldCap << 1nextTable(新数组)transferIndex 设为 newTable.lengthtransferIndex 领取迁移任务,各自负责一段桶位的迁移transferIndex 向前领取一段桶位(如 16 个)table[i] 的节点迁移到 nextTableForwardingNode,表示“该桶已迁移”// 伪代码
while (transferIndex > 0) {
int idx = transferIndex - 1;
transferIndex = idx;
// 迁移 table[idx]
}ForwardingNode 是一个特殊的节点,hash = MOVED (-1)nextTableForwardingNode,就会自动协助迁移或直接去 nextTable 查找if (f.hash == MOVED) {
// 表示正在扩容,协助迁移
tab = helpTransfer(tab, f);
}✅ 优势:读写线程都能参与扩容,极大提升扩容效率。
特性 | JDK 1.7 | JDK 1.8 |
|---|---|---|
锁机制 | Segment 继承 ReentrantLock | synchronized 锁桶位头节点 |
锁粒度 | Segment 级(默认 16 个) | 桶位级(每个 Node 一个锁) |
CAS 使用 | 较少 | 广泛用于初始化、插入、控制变量 |
数据结构 | Segment + HashEntry | Node + 链表/红黑树 |
扩容机制 | 单线程扩容,Segment 独立 | 多线程协助扩容,全局迁移 |
volatile 使用 | 有限 | table、sizeCtl 等广泛使用 |
性能 | 较高,但结构复杂 | 更高,结构简单,适合多核 |
✅ 总结:JDK 1.8 的设计更简洁、更高效,充分利用了 CAS 和多线程协作,是现代并发编程的典范。
if (key == null || value == null) throw new NullPointerException();原因很简单:避免歧义。
get(key) 返回 null,可能是“键不存在”,也可能是“值为 null”而 HashMap 允许 null,是因为它是单线程使用的,开发者可以自行判断。
ConcurrentHashMap 从 JDK 1.7 到 JDK 1.8 的演进,是一次从“分而治之”到“无锁化 + 细粒度锁”的思想跃迁。
版本 | 核心思想 | 关键技术 |
|---|---|---|
JDK 1.7 | 分段锁 | Segment + ReentrantLock |
JDK 1.8 | 无锁化 + 桶位锁 | CAS + synchronized + volatile + 多线程扩容 |