Java ConcurrentHashMap源码深度解析:从底层原理到性能优化
引言
ConcurrentHashMap 是 Java 并发编程中非常核心的类,它在保证线程安全的同时,提供了极高的并发性能。与 Hashtable 相比,ConcurrentHashMap 通过分段锁(Segment)或更先进的 CAS + Synchronized 机制,避免了全局锁带来的性能瓶颈。本文将从源码级别深入剖析 ConcurrentHashMap 的设计思想、数据结构、核心方法实现以及性能优化策略。
1. 历史演进:从 JDK 7 到 JDK 8
1.1 JDK 7: Segment 分段锁机制
JDK 7 中的 ConcurrentHashMap 采用 分段锁(Segment)的设计。其核心思想是将整个哈希表划分为多个段(Segment),每个段独立加锁,从而允许多个线程同时操作不同的段,提升并发能力。
- 数据结构 :
Segment<K,V>[] segments,每个 Segment 实际上是一个小型的HashEntry表。 - 锁粒度:锁作用于 Segment,而非整个 Map。
- 缺点:虽然提升了并发性,但依然存在锁竞争和扩容时的性能问题。
1.2 JDK 8: CAS + Synchronized + Node 数组 + 链表/红黑树结构
JDK 8 对 ConcurrentHashMap 进行了重大重构,摒弃了 Segment,引入了更高效的 CAS 操作 + synchronized 锁 + 红黑树 的组合。
- 数据结构 :
Node<K,V>[] table,数组 + 链表 + 红黑树。 - 锁粒度 :锁作用于 桶(bucket),即链表或红黑树的头节点。
- 核心优势:锁的粒度更细,极大减少了锁竞争,提升了高并发下的性能。
2. 核心数据结构解析(JDK 8)
2.1 Node 结构
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
}
hash: 键的哈希值,用于定位桶位置。key: 键,不可变。val: 值,使用volatile保证可见性。next: 指向下一个节点,构成链表。
2.2 TreeNode 与 TreeBin
当链表长度超过阈值(默认为 8),会转换为红黑树以提高查找效率。
java
static final class TreeNode<K,V> extends Node<K,V> {
// 红黑树节点结构,包含 parent, left, right 等指针
}
static final class TreeBin<K,V> {
TreeNode<K,V> root;
// 其他字段...
}
3. 核心方法源码分析(JDK 8)
3.1 put(K key, V value) - 插入操作
3.1.1 步骤分解
- 计算 hash :
hash = spread(key.hashCode()),防止哈希冲突。 - 判断是否需要初始化 :如果
table == null,调用initTable()进行初始化。 - 定位桶位置 :
i = (n - 1) & hash,其中n是 table 的长度。 - CAS 检查空桶 :若桶为空,使用
casTabAt(table, i, null, new Node<>(hash, key, value, null))原子插入。 - 处理链表/红黑树:如果桶不为空,且是链表,则遍历链表;如果是红黑树,则调用红黑树插入方法。
- 同步锁 :对桶的头节点加
synchronized锁,确保修改的原子性。 - 扩容检查 :如果插入后元素数量超过阈值,触发
transfer()扩容。
3.1.2 CAS 与 Synchronized 的协同
- CAS:用于无锁场景(如空桶插入)。
- Synchronized:用于有竞争的场景(如链表插入),锁住的是头节点,而不是整个 map。
关键点:锁的粒度是「桶」,而非「整个 map」,这是性能提升的核心。
3.2 get(K key) - 读取操作
java
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e; 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 && ((ek = e.key) == key || (ek != null && ek.equals(key))))
return e.val;
while ((e = e.next) != null) {
if (e.hash == h && ((ek = e.key) == key || (ek != null && ek.equals(key))))
return e.val;
}
}
return null;
}
- 特点 :完全无锁 !读操作不加任何锁,仅依赖
volatile保证可见性。 - 优势:读操作性能极高,几乎不受并发影响。
3.3 resize() / transfer() - 扩容机制
- 并发扩容 :不同于
HashMap,ConcurrentHashMap支持并发扩容。 - 工作方式 :多个线程可以共同参与扩容,将旧 table 中的元素迁移到新 table,通过
sizeCtl变量协调。 - 避免死锁 :使用
CAS检查并设置sizeCtl来控制扩容状态。
4. 性能优化策略总结
| 优化点 | 说明 |
|---|---|
| 细粒度锁 | 锁作用于桶头节点,极大减少锁竞争 |
| CAS 无锁操作 | 在无竞争时,通过 CAS 实现原子更新 |
| 读操作无锁 | 读取不加锁,利用 volatile 保证可见性 |
| 红黑树优化 | 链表过长时转为红黑树,降低查找时间复杂度 |
| 并发扩容 | 多线程协作完成扩容,避免阻塞 |
5. 使用建议与注意事项
- 不要在遍历时修改集合 :
ConcurrentHashMap不支持在迭代时修改,否则会抛出ConcurrentModificationException。 - 避免键或值为 null :虽然允许,但可能导致
get()返回null无法区分是不存在还是值为 null。 - 合理设置初始容量:可减少扩容次数,提升性能。
- 优先使用
computeIfAbsent等原子操作:避免手动同步代码块。
总结
ConcurrentHashMap 是 Java 并发编程中的典范之作。它通过 分段锁 → CAS+Synchronized 的演进,实现了高性能与线程安全的完美平衡。理解其源码不仅有助于我们写出更高效的并发代码,也为我们学习其他并发容器(如 ConcurrentSkipListMap)打下坚实基础。
推荐 :结合 JDK 8 源码阅读,重点关注
put,get,resize方法的实现细节。