Java 8 对 ConcurrentHashMap
进行了重大优化,摒弃了分段锁(Segment)的设计,转而采用更高效的 CAS(Compare-And-Swap) + synchronized 结合的方式,显著提升了并发性能。以下是其核心原理、实现细节及使用场景的详细解析。
一、核心设计思想
-
目标:
- 在高并发场景下,提供线程安全且高效的哈希表操作。
- 通过细粒度锁 (仅锁定单个哈希桶)和无锁读 (
volatile
变量)减少竞争。
-
关键优化:
- 链表转红黑树:解决哈希冲突时链表查询效率低的问题。
- 多线程协同扩容:避免全表锁定的开销。
二、数据结构
1. 核心成员变量
java
java
transient volatile Node<K,V>[] table; // 哈希桶数组
private transient volatile int sizeCtl; // 控制表初始化和扩容的标记
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val; // 值用 volatile 修饰,保证可见性
volatile Node<K,V> next; // 链表指针
}
-
哈希桶数组(table) :每个桶是一个链表或红黑树(
TreeBin
节点)。 -
sizeCtl:
-1
:表正在初始化。-N
:有N-1
个线程正在扩容。- 正数:表未初始化时表示初始容量;初始化后表示扩容阈值。
2. 链表转红黑树
- 触发条件:当链表长度 ≥8 且哈希表容量 ≥64 时,链表转为红黑树。
- 回退条件:当红黑树节点数 ≤6 时,退化为链表。
- 目的 :将链表查询的
O(n)
时间复杂度优化为红黑树的O(log n)
。
3. TreeBin 节点
scala
static final class TreeBin<K,V> extends Node<K,V> {
TreeNode<K,V> root; // 红黑树根节点
volatile TreeNode<K,V> first; // 链表头节点(遍历用)
volatile Thread waiter; // 等待线程
volatile int lockState; // 锁状态(CAS 控制)
}
- 红黑树的操作通过
TreeBin
封装,内部通过CAS
和synchronized
保证线程安全。
三、扩容机制
触发条件 : • 当哈希表中的元素数量超过 容量 × 负载因子(默认 0.75)
。 • 单次插入后,若发现当前桶已迁移完毕且需要扩容,触发协作扩容。
扩容流程:
-
初始化新数组: 新数组大小为原数组的 2 倍(例如从 16 扩容到 32)。
iniNode<K,V>[] nextTable = new Node[n << 1];
-
分配迁移任务: 将旧数组的桶分为多个区间,每个线程负责迁移一部分区间。
-
多线程协作迁移 : • 步长(stride) :每个线程每次处理一个桶区间(如 16 个桶)。 • 高低位拆分 : 将旧桶中的链表或红黑树节点按哈希值的高低位拆分到新数组的两个位置(
i
和i + n
)。arduino// 示例:旧桶索引为 i,新桶索引为 i 或 i + n if ((e.hash & n) == 0) { // 低位链表 } else { // 高位链表 }
-
迁移完成标志 : 迁移完成后,更新哈希表引用
table = nextTable
,并更新sizeCtl
为新的扩容阈值。
协作扩容 : • 其他线程在插入时若检测到正在扩容(Node.hash == MOVED
),会调用 helpTransfer()
协助迁移。 • 源码片段:
javascript
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
if (tab != null && f instanceof ForwardingNode) {
// 协助迁移数据
}
}
四、删除操作
流程:
- 定位哈希桶 : 根据键的哈希值找到对应的桶(索引为
(n - 1) & hash
)。 - 处理链表或红黑树 : • 链表 :遍历链表找到目标节点,修改前驱节点的
next
指针。 • 红黑树 :通过TreeBin
的removeTreeNode()
方法删除节点,并调整树结构。 - 原子替换或 CAS 操作 : • 若删除的是头节点,使用
CAS
替换头节点。 • 若删除的是中间节点,需在同步块中操作(通过synchronized
锁定头节点)。
源码示例(链表删除):
scss
synchronized (f) {
if (tabAt(tab, i) == f) {
// 遍历链表删除节点
if (prev != null) {
prev.next = e.next;
} else {
setTabAt(tab, i, e.next);
}
}
}
红黑树删除 : • 调用 TreeBin
的 removeTreeNode()
方法,平衡红黑树结构。 • 若树节点数 ≤6,将红黑树退化为链表。
五、获取操作
流程:
-
计算哈希值:
iniint hash = spread(key.hashCode());
-
定位哈希桶 : 根据哈希值找到对应的桶(索引为
(n - 1) & hash
)。 -
遍历链表或红黑树 : • 链表 :从头节点开始遍历,匹配
key
和hash
。 • 红黑树 :调用TreeBin
的find()
方法,在树中搜索节点。
无锁读实现 : • 所有 Node.val
和 Node.next
字段均用 volatile
修饰,保证可见性。 • 红黑树的 TreeBin
通过 volatile
状态标记确保读操作安全。
源码示例:
kotlin
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) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
} else if (eh < 0) // 红黑树查找
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { // 遍历链表
if (e.hash == h && ((ek = e.key) == key || key.equals(ek)))
return e.val;
}
}
return null;
}
五、链表转红黑树(树化)
触发条件 : • 链表长度 ≥ TREEIFY_THRESHOLD
(默认 8)。 • 哈希表容量 ≥ MIN_TREEIFY_CAPACITY
(默认 64)。
树化流程:
-
锁定桶头节点 : 使用
synchronized
锁定头节点,防止并发修改。 -
创建 TreeBin : 将链表节点转换为红黑树,并创建
TreeBin
对象作为新的头节点。scssTreeBin<K,V> tb = new TreeBin<K,V>(); tb.addAll(head); // 将链表节点添加到红黑树 setTabAt(tab, index, tb); // 替换链表为 TreeBin
-
维护双向链表 :
TreeBin
内部维护一个双向链表,支持按插入顺序遍历。
退化为链表 : • 当红黑树节点数 ≤ UNTREEIFY_THRESHOLD
(默认 6)时,调用 untreeify()
方法将树转为链表。
源码示例:
ini
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1); // 容量不足时扩容
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
// 将链表转为红黑树
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p = new TreeNode<>(e.hash, e.key, e.val, null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
五、线程安全实现
- CAS 无锁操作 : • 头节点插入、
sizeCtl
修改等通过CAS
实现无锁化。 • 示例:casTabAt(tab, i, null, new Node(...))
。 - synchronized 锁: • 哈希冲突时锁定桶的头节点,保证链表或树操作的原子性。
- volatile 变量 : •
table
数组、Node.val
、Node.next
等字段用volatile
修饰,保证可见性。
六、对比 Java 7 与 Java 8 的设计
特性 | Java 7(分段锁) | Java 8(桶锁 + CAS) |
---|---|---|
锁粒度 | 段锁(默认 16 段) | 桶锁(仅锁定单个桶的头节点) |
哈希冲突处理 | 链表 | 链表转红黑树(优化查询效率) |
扩容机制 | 单线程扩容 | 多线程协作扩容 |
内存开销 | 高(每个段维护独立哈希表) | 低(统一哈希表结构) |
读性能 | 无锁读但需遍历段 | 无锁读且直接访问目标桶 |
七、适用场景总结
- 高并发读写:如缓存系统、实时计算中间状态存储。
- 动态扩容需求:支持多线程协作扩容,避免长时间阻塞。
- 复杂哈希冲突:通过红黑树优化长链表的查询效率。
八、注意事项
- 避免哈希冲突不均匀:设计良好的哈希函数,防止大量键映射到同一桶。
- 合理初始化容量:根据预估数据量设置初始容量,减少扩容次数。
- 慎用 size() 和 mappingCount() : •
size()
返回的是近似值,不保证精确性。 •mappingCount()
返回long
类型的更准确计数。