1.7版本
-
1.7版本的ConcurrentHashMap是基于Segment分段实现的
-
每个Segment相对于⼀个⼩型的HashMap
-
每个Segment内部会进⾏扩容,和HashMap的扩容逻辑类似
-
先⽣成新的数组,然后转移元素到新数组中
-
扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
-
1.8版本的ConcurrentHashMap不再基于Segment实现
-
当某个线程进⾏put时,如果发现ConcurrentHashMap正在进⾏扩容那么该线程⼀起进⾏扩容
-
如果某个线程put时,发现没有正在进⾏扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进⾏扩容
-
ConcurrentHashMap是⽀持多个线程同时扩容的
-
扩容之前也先生成⼀个新的数组
-
在转移元素时,先将原数组分组,将每组分给不同的线程来进⾏元素的转移,每个线程负责⼀组或多组的元素转移⼯作
ConcurrentHashMap 的扩容机制
在 Java 中,ConcurrentHashMap
是一个线程安全的哈希表,它的扩容机制与 HashMap
有一些相似之处,但由于它支持并发操作,扩容时需要额外的同步控制,以保证线程安全。
1. ConcurrentHashMap 的数据结构
在 JDK 1.8 及以后,ConcurrentHashMap
采用 数组 + 链表 + 红黑树 的结构:
- 数组 (
Node<K,V>[] table
):存储键值对的主要结构。 - 链表:当哈希冲突发生时,链表存储多个键值对。
- 红黑树:当链表长度超过 8 时,链表会转换为红黑树,以提高查询效率。
2. 触发扩容的条件
当 ConcurrentHashMap
的 实际存储元素数量 (size()
) 超过扩容阈值 (threshold
) 时,触发扩容:
- 扩容阈值 = 数组长度 × 负载因子 (默认负载因子
0.75
)。 - 默认初始大小 为
16
,即threshold = 16 × 0.75 = 12
。 - 当
size()
超过12
时,触发扩容,数组容量翻倍。
3. 扩容的具体流程
扩容的核心是 创建一个新的数组(容量是原来的 2 倍),并将旧数据迁移到新数组。流程如下:
(1) 计算新容量
- 扩容后的 新数组容量 = 旧数组容量 × 2。
- 计算新的扩容阈值
newThreshold = newCapacity × loadFactor
。
(2) CAS 竞争控制扩容
ConcurrentHashMap
采用 分段迁移(分批扩容),避免所有线程都在扩容时造成阻塞。- 使用
sizeCtl
控制扩容状态:sizeCtl < 0
表示有线程正在扩容,其他线程会协助迁移数据。sizeCtl > 0
表示扩容门槛,控制扩容的触发。
(3) 迁移数据(Rehashing)
数据迁移采用 多线程协作迁移:
ConcurrentHashMap
通过transfer()
方法 进行数据迁移。- 每个线程会尝试搬移部分数据(桶的迁移是按批次完成的)。
- 迁移过程中,如果旧桶内元素是:
- 单个节点(链表长度 ≤ 1):直接移动到新数组中的位置。
- 链表节点(链表长度 > 1):会重新计算哈希值,可能拆分成两个新的链表。
- 红黑树节点:如果还是满足红黑树条件,则保留红黑树结构,否则降级回链表。
(4) 迁移完成,更新引用
- 迁移完成后,旧表引用会被丢弃,
table
指向新的扩容后的数组。 sizeCtl
重新设置为新的扩容阈值,表示扩容完成。
4. 扩容的并发优化
(1) 分批迁移
不像 HashMap
需要一次性迁移所有数据,ConcurrentHashMap
采用 分段式扩容:
- 迁移时,每个线程处理 一个桶(bucket),加速迁移过程。
- 其他线程可以并发参与数据迁移,提高效率,避免单线程阻塞。
(2) 低冲突的哈希算法
ConcurrentHashMap
采用 MurmurHash 变种 计算索引,减少哈希冲突,使数据更均匀地分布在不同桶中,降低扩容的负担。
5. 总结
扩容的特点
- 触发条件:当
size()
超过threshold = capacity × 0.75
时触发扩容。 - 新容量计算:新容量 = 旧容量 × 2,迁移数据到新的桶。
- 并发优化:
- 分批迁移:多个线程可以并发搬移数据,加速迁移过程。
- CAS 竞争控制:防止多个线程同时初始化扩容。
- 数据迁移方式:
- 单个节点直接复制。
- 链表可能拆分成两个部分。
- 红黑树保持结构或降级为链表。
对比 HashMap
特性 | HashMap | ConcurrentHashMap |
---|---|---|
触发扩容 | size > threshold | size > threshold |
容量增长 | capacity × 2 | capacity × 2 |
扩容方式 | 单线程 一次性迁移所有数据 | 多线程分批迁移,避免阻塞 |
并发安全 | 非线程安全 | 线程安全,支持高并发 |
6. 相关源码
关键代码解析
(1) transfer()
方法
private final void transfer(Node<K,V>[] oldTab, Node<K,V>[] newTab) {
int newCap = newTab.length;
for (int i = 0; i < oldTab.length; i++) {
Node<K,V> e;
if ((e = oldTab[i]) != null) {
oldTab[i] = null; // 清空旧数组的桶
if (e.next == null) {
newTab[e.hash & (newCap - 1)] = e; // 直接迁移
} else if (e instanceof TreeNode) {
// 红黑树迁移
((TreeNode<K,V>)e).split(newTab, i, oldCap);
} else {
// 处理链表拆分
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null) loHead = e;
else loTail.next = e;
loTail = e;
} else {
if (hiTail == null) hiHead = e;
else hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[i] = loHead; // 低位链表
}
if (hiTail != null) {
hiTail.next = null;
newTab[i + oldCap] = hiHead; // 高位链表
}
}
}
}
}
关键点解析:
- 单节点直接迁移:如果桶中只有一个元素,直接迁移到新数组。
- 链表迁移:
- 使用
(e.hash & oldCap) == 0
进行拆分,数据要么留在原索引,要么移动到oldCap + i
的位置。 - 避免重新计算哈希值,提升性能。
- 使用
- 红黑树迁移:保持红黑树结构,提升查询性能。
7. 结论
ConcurrentHashMap
采用分批扩容,使多个线程可以并发进行数据迁移,避免单线程扩容的性能瓶颈。- 迁移时,链表可能拆分成两个部分,红黑树保持结构或降级为链表。
- 扩容的 主要优化点 是 分批迁移 + CAS 竞争控制,适合高并发场景。