当前位置: 首页 > news >正文

对比 HashMap 和 ConcurrentHashMap 扩容逻辑的差异

HashMapConcurrentHashMap扩容逻辑 上有明显的差异,尤其是在并发环境下的处理策略,这是它们核心区别之一。


🧱 一、总体对比表(JDK 8 为例)

特性HashMapConcurrentHashMap
线程安全❌ 否✅ 是
是否支持并发扩容❌ 否,单线程触发并执行✅ 是,多线程协助扩容
是否使用锁❌ 否✅ 使用 synchronized / CAS / volatile
触发扩容时机size >= threshold同样
扩容粒度一次全部迁移分段迁移,线程协助
链表拆分逻辑hash & oldCap 拆分同上,但更复杂考虑线程安全

🧩 二、HashMap 扩容逻辑(单线程)

  1. 判断是否超过阈值;
  2. 创建新数组,长度为原来的 2 倍;
  3. 遍历旧数组,将每个桶内的链表/树拆分并移动到新数组;
  4. 所有数据复制完成后替换原数组引用;
  5. 单线程完成,扩容过程期间会阻塞写操作(可能数据丢失)

特点:

  • 简单、效率高;
  • 不适合并发,多线程操作时会导致死循环(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 拆分类似)
    }
}

⚠️ 四、重点区别总结

比较维度HashMapConcurrentHashMap
扩容线程单线程多线程协作
是否线程安全❌ 否✅ 是
是否阻塞写操作是(扩容期间)否,允许边扩边写
桶迁移方式一次性整体迁移分段迁移,ForwardingNode 标记
扩容中可否 put❌ 一般卡住✅ 可 put,会协助迁移

✅ 总结一张图

HashMap:
      单线程
     ──────────► resize()
                   ↑
           所有 put 操作都阻塞


ConcurrentHashMap:
        多线程协作
        ┌─────┬──────┐
     put()  put()   put()
       ↓      ↓       ↓
  发现需要扩容,参与 helpTransfer()
       ↓      ↓       ↓
   每个线程搬自己那一段

我们继续深入解析 ConcurrentHashMap 扩容过程中的多线程协作,并提供一个简化的示例,展示多个线程如何协作进行扩容搬迁

🧩 五、ConcurrentHashMap 扩容多线程协作详解

扩容的核心思想:

  1. 多个线程并行参与扩容,但每个线程处理自己负责的一部分桶(通过 transferIndex 划分任务);
  2. 每个线程通过 helpTransfer() 协助其他线程迁移数据;
  3. 在扩容期间,ForwardingNode 被用来占位,标识该桶已经完成搬迁,其他线程可避免重复迁移。

扩容过程简述:

  • 触发扩容时,transferIndex 会标记当前桶的搬迁进度。
  • 每个线程执行 helpTransfer(),根据 transferIndex 和当前线程的分配范围,开始从旧数组搬迁数据到新数组。

扩容过程中的关键操作:

  1. ForwardingNode:一种特殊的占位符节点,指示该桶已经从旧数组迁移至新数组。
  2. 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. 解释

  • NodeForwardingNodeNode 是标准的链表节点,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
...

扩容后,多个线程协作成功地将数据从旧数组迁移到新数组,并且不会丢失数据。


✅ 七、总结

  1. HashMap 的扩容 是单线程的,整个过程会被阻塞,且扩容过程中可能会丢失数据或导致性能问题。
  2. ConcurrentHashMap 的扩容 采用了多线程协作机制,多个线程可以并行处理不同的桶,确保扩容期间依然能够处理插入操作,且不会丢失数据。
  3. ForwardingNode 在扩容过程中起到了占位符的作用,标识该桶已经迁移,避免重复迁移。

通过以上多线程协作的示例,我们可以更清楚地看到 ConcurrentHashMap 扩容的并发优化,并理解如何通过分段迁移来保证线程安全。

相关文章:

  • struct结构体、union联合体和枚举
  • 第1节:计算机视觉发展简史
  • python headq包介绍
  • 重组人碱性成纤维细胞生长因子 (rh-bFGF)-促进组织/伤疤修复
  • JAVA 双括号初始化
  • 消防设施操作员考试重点考哪些内容呢?
  • Byte-Buddy系列 - 第2讲 方法拦截与委托实现
  • 1ll C++
  • STM32F407使用ESP8266连接阿里云并上传数据
  • MySQL【8.0.41版】安装详细教程--无需手动配置环境
  • 热Key问题及其解决方案:Redis高并发场景下的性能优化
  • 因子分析学习介绍,及其高阶应用以及学术上创新方向,鲁棒因子分析建模
  • [ACM_3] n组数据 | getchar() | getline(cin,s)
  • 代码重构学习
  • 塔能物联运维:成功打破物联网设备之间的互联互通难题
  • 蓝桥杯备赛学习笔记:高频考点与真题预测(C++/Java/python版)
  • Java 中序列化和反序列化
  • C语言【模仿strcpy】
  • kkFileView的安装和使用
  • fabric test-network启动
  • 青岛专业设计网站公司/新郑网络推广外包
  • 做网站怎么给客户打电话/哪家培训机构学校好
  • 企业查查app下载/网站关键词优化的价格
  • 做网站 后端是谁来做的/网站推广的渠道有哪些
  • 做网站需要ps吗/怎么在百度发布免费广告
  • 龙岩网站建设推广/雅虎搜索引擎入口