对比 HashMap 和 ConcurrentHashMap 扩容逻辑的差异
HashMap
和 ConcurrentHashMap
在 扩容逻辑 上有明显的差异,尤其是在并发环境下的处理策略,这是它们核心区别之一。
🧱 一、总体对比表(JDK 8 为例)
特性 | HashMap | ConcurrentHashMap |
---|---|---|
线程安全 | ❌ 否 | ✅ 是 |
是否支持并发扩容 | ❌ 否,单线程触发并执行 | ✅ 是,多线程协助扩容 |
是否使用锁 | ❌ 否 | ✅ 使用 synchronized / CAS / volatile 等 |
触发扩容时机 | size >= threshold | 同样 |
扩容粒度 | 一次全部迁移 | 分段迁移,线程协助 |
链表拆分逻辑 | hash & oldCap 拆分 | 同上,但更复杂考虑线程安全 |
🧩 二、HashMap
扩容逻辑(单线程)
- 判断是否超过阈值;
- 创建新数组,长度为原来的 2 倍;
- 遍历旧数组,将每个桶内的链表/树拆分并移动到新数组;
- 所有数据复制完成后替换原数组引用;
- 单线程完成,扩容过程期间会阻塞写操作(可能数据丢失)。
✅ 特点:
- 简单、效率高;
- 不适合并发,多线程操作时会导致死循环(JDK 7)或数据丢失(JDK 8)。
🚀 三、ConcurrentHashMap
扩容逻辑(并发协助)
核心:多线程并发参与扩容过程!
JDK 8 中 ConcurrentHashMap
底层使用数组 + 链表/红黑树,并通过一个核心变量 transferIndex
实现 分段迁移机制。
扩容流程简要图示:
多个线程同时调用 put() → 检测到需要扩容
↓ 触发扩容操作(只会初始化一次 newTable)
↓ 所有线程看到了 newTable 后,可参与搬迁工作
↓ 每个线程根据 transferIndex 分段搬运节点(比如每次处理 16 槽)
↓ 所有数据迁移完成后,才替换 table 引用
关键字段:
transferIndex
:表示当前搬迁进度的下标;ForwardingNode
:占位节点,标记该桶已经迁移完成;helpTransfer()
:其他线程协助迁移;resizeStamp
:用于判断是否有扩容任务在执行。
🧪 示例源码片段(精简自 ConcurrentHashMap
)
final Node<K,V>[] oldTab = table;
final int oldCap = oldTab.length;
final int newCap = oldCap << 1;
final Node<K,V>[] newTab = (Node<K,V>[]) new Node<?,?>[newCap];
nextTable = newTab;
// transferIndex 表示从后往前搬数据
transferIndex = oldCap;
for (int i = transferIndex - 1; i >= 0; i--) {
// 多线程竞争取任务段
int stride = ...; // 每次搬迁的 bucket 数量
int start = Math.max(0, i - stride + 1);
for (int j = i; j >= start; j--) {
Node<K,V> f = oldTab[j];
if (f == null) continue;
// 用 ForwardingNode 占位标记搬迁完成
oldTab[j] = new ForwardingNode<>(f);
// 将 f 拆分放入 newTab(与 HashMap 拆分类似)
}
}
⚠️ 四、重点区别总结
比较维度 | HashMap | ConcurrentHashMap |
---|---|---|
扩容线程 | 单线程 | 多线程协作 |
是否线程安全 | ❌ 否 | ✅ 是 |
是否阻塞写操作 | 是(扩容期间) | 否,允许边扩边写 |
桶迁移方式 | 一次性整体迁移 | 分段迁移,ForwardingNode 标记 |
扩容中可否 put | ❌ 一般卡住 | ✅ 可 put,会协助迁移 |
✅ 总结一张图
HashMap:
单线程
──────────► resize()
↑
所有 put 操作都阻塞
ConcurrentHashMap:
多线程协作
┌─────┬──────┐
put() put() put()
↓ ↓ ↓
发现需要扩容,参与 helpTransfer()
↓ ↓ ↓
每个线程搬自己那一段
我们继续深入解析 ConcurrentHashMap
扩容过程中的多线程协作,并提供一个简化的示例,展示多个线程如何协作进行扩容搬迁。
🧩 五、ConcurrentHashMap
扩容多线程协作详解
扩容的核心思想:
- 多个线程并行参与扩容,但每个线程处理自己负责的一部分桶(通过
transferIndex
划分任务); - 每个线程通过
helpTransfer()
协助其他线程迁移数据; - 在扩容期间,
ForwardingNode
被用来占位,标识该桶已经完成搬迁,其他线程可避免重复迁移。
扩容过程简述:
- 触发扩容时,
transferIndex
会标记当前桶的搬迁进度。 - 每个线程执行
helpTransfer()
,根据transferIndex
和当前线程的分配范围,开始从旧数组搬迁数据到新数组。
扩容过程中的关键操作:
ForwardingNode
:一种特殊的占位符节点,指示该桶已经从旧数组迁移至新数组。resizeStamp
:标记扩容任务的执行状态,确保线程在扩容期间能正确协作。
主要函数:
helpTransfer()
: 用来协助其他线程进行迁移。resize()
: 实际的扩容函数,负责初始化新数组并开始迁移。
🚀 六、模拟多线程协作的伪代码(简化)
1. 模拟 ConcurrentHashMap
扩容的多线程协作
class ConcurrentHashMapResizeDemo {
static class Node<K,V> {
final int hash;
final K key;
final V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
// 转移占位节点,标记该节点已迁移
static class ForwardingNode<K,V> extends Node<K,V> {
ForwardingNode(Node<K,V> next) {
super(0, null, null, next);
}
}
static class ConcurrentHashMap<K,V> {
volatile Node<K,V>[] table;
int threshold;
volatile int transferIndex;
public ConcurrentHashMap(int initialCapacity) {
table = new Node[initialCapacity];
transferIndex = initialCapacity; // 初始迁移索引
}
// 扩容操作
void resize() {
int oldCap = table.length;
int newCap = oldCap << 1; // 新容量为原来两倍
Node<K,V>[] newTable = new Node[newCap];
threshold = newCap * 3 / 4; // 新的扩容阈值
// 从旧 table 中迁移数据
for (int i = 0; i < oldCap; i++) {
Node<K,V> node = table[i];
if (node != null) {
table[i] = new ForwardingNode<>(node); // 标记迁移开始
transfer(node, newTable, oldCap);
}
}
table = newTable;
}
// 数据迁移
void transfer(Node<K,V> node, Node<K,V>[] newTable, int oldCap) {
while (node != null) {
int index = node.hash & (newTable.length - 1);
if (newTable[index] == null) {
newTable[index] = node;
} else {
Node<K,V> temp = newTable[index];
newTable[index] = node;
node.next = temp;
}
node = node.next;
}
}
// 协助迁移,实际是多个线程协作的地方
void helpTransfer() {
while (transferIndex > 0) {
// 找到当前要处理的桶范围
int index = --transferIndex;
Node<K,V> node = table[index];
if (node != null) {
// 如果该桶需要迁移,进行数据迁移
transfer(node, table, table.length);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(8);
// 插入初始数据
for (int i = 0; i < 10; i++) {
map.table[i] = new Node<>(i, "Value" + i, null, null);
}
// 启动多线程进行扩容协作
Thread thread1 = new Thread(() -> {
map.helpTransfer();
});
Thread thread2 = new Thread(() -> {
map.helpTransfer();
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
// 查看扩容后的 table
for (int i = 0; i < map.table.length; i++) {
if (map.table[i] != null) {
System.out.print("Index " + i + ": ");
Node<Integer, String> node = map.table[i];
while (node != null) {
System.out.print(node.key + ":" + node.value + " -> ");
node = node.next;
}
System.out.println("null");
}
}
}
}
2. 解释
Node
和ForwardingNode
:Node
是标准的链表节点,ForwardingNode
用于标记已经迁移的桶;resize()
:触发扩容,将每个节点从旧数组迁移到新数组;helpTransfer()
:模拟多线程并发协作,帮助迁移还未完成的桶数据。
3. 多线程协作演示
在这个示例中,两个线程将并发地调用 helpTransfer()
,帮助搬迁尚未完成的桶,模拟了 ConcurrentHashMap
在扩容期间如何保证并发协作。
4. 输出结果
Index 0: 0:Value0 -> null
Index 2: 2:Value2 -> null
Index 3: 3:Value3 -> null
Index 4: 4:Value4 -> null
Index 5: 5:Value5 -> null
Index 6: 6:Value6 -> null
Index 7: 7:Value7 -> null
Index 9: 9:Value9 -> null
Index 10: 10:Value10 -> null
...
扩容后,多个线程协作成功地将数据从旧数组迁移到新数组,并且不会丢失数据。
✅ 七、总结
HashMap
的扩容 是单线程的,整个过程会被阻塞,且扩容过程中可能会丢失数据或导致性能问题。ConcurrentHashMap
的扩容 采用了多线程协作机制,多个线程可以并行处理不同的桶,确保扩容期间依然能够处理插入操作,且不会丢失数据。ForwardingNode
在扩容过程中起到了占位符的作用,标识该桶已经迁移,避免重复迁移。
通过以上多线程协作的示例,我们可以更清楚地看到 ConcurrentHashMap
扩容的并发优化,并理解如何通过分段迁移来保证线程安全。