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();// 查看扩容后的 tablefor (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
扩容的并发优化,并理解如何通过分段迁移来保证线程安全。