ConcurrentHashMap 核心原理解析

3 阅读2分钟

一、 数据结构:坚实的基石

JDK 1.8 中,ConcurrentHashMap 摒弃了之前的分段锁(Segment)设计,转而采用了与 HashMap 类似的 数组 + 链表 + 红黑树 的结构。

  • 核心改进:利用 synchronizedCAS (Compare And Swap) 来保证线程安全。
  • 存储单元:使用 Node 数组。当链表长度超过 8 且数组容量大于 64 时,链表会转换为红黑树以优化查询效率。

二、 putVal() 方法:高并发写入的艺术

putVal() 是整个类中最核心的逻辑。它并没有使用全局锁,而是通过细粒度的控制来实现并发。

关键步骤解读:

  1. 无限循环 (forndoe) :确保数据一定能插入成功。

  2. 检查空表:如果 table 未初始化,先执行初始化。

  3. CAS 插入头节点:计算 Hash 位置,如果该位置为空,直接通过 CAS 尝试放置新节点。如果成功,直接跳出。

  4. 检测扩容 (MOVED) :如果发现节点 Hash 值为 -1,说明正在扩容,当前线程会加入协助扩容。

  5. 锁定头节点 (synchronized) :如果该位置已有数据且非扩容态,则对该桶的头节点加锁

    • 若是链表,遍历寻找 Key,找到则更新,没找到则尾插。
    • 若是红黑树,按照红黑树规则插入。
  6. 树化检查:插入完成后,判断是否需要将链表转为红黑树。

注意: ConcurrentHashMap 不允许 null 作为 Key 或 Value,这是为了避免在并发环境下的歧义(无法区分是“值不存在”还是“值为 null”)。


三、 get() 方法:极致的查询性能

相较于复杂的写入,get() 方法非常简洁,且全程不加锁

关键步骤解读:

  1. 计算 Hash:计算 Key 的 Hash 值。

  2. 定位桶位置:如果头节点直接匹配,返回结果。

  3. 多态查找

    • 如果头节点的 hash < 0(说明是红黑树或者是正在扩容的 ForwardingNode),调用对应节点的 find() 方法进行查找。
    • 如果是普通链表,循环遍历。
  4. 返回结果:找不到则返回 null

为什么 get() 不需要加锁?

这是因为 Node 内部的 valnext 指针都使用了 volatile 修饰:

volatile V val;volatile\ V\ val;

volatile Node<K,V> next;volatile\ Node<K,V>\ next;

这保证了当一个线程修改了节点值或插入了新节点,其他线程能立即看到最新的变化(可见性)。


四、 总结

  • 锁粒度:从 JDK 1.7 的 Segment 级别优化到了 JDK 1.8 的 Node 级别,并发度更高。
  • 性能:读操作完全无锁,写操作局部加锁。
  • 适用场景:高并发下的键值对存储,是构建本地缓存的首选工具。