目录

在-JDK-1.8-的-ConcurrentHashMap-中,为什么存在两种插入方式

在 JDK 1.8 的 ConcurrentHashMap 中,为什么存在两种插入方式?

在 JDK 1.8 的 ConcurrentHashMap 中,之所以对“容器为空”和“计算位置为空”采取不同的处理方式,主要是因为 并发场景下的性能优化和并发安全保证 。我们可以分开来看这两种情况:

1. 容器为空时,使用 volatile + CAS 初始化

  • 原因ConcurrentHashMap 采用 懒加载 ,并不会在构造时就初始化所有桶( Node<K, V>[] table )。
  • 实现 :当第一次插入元素时,会先判断 table 是否为空:
if (tab == null || (n = tab.length) == 0) 
    tab = initTable();
  • initTable() 方法使用 CAS(Compare-And-Swap) 操作来保证线程安全的初始化。

  • 为什么用 CAS 而不是 synchronized ?

    • 目的是减少不必要的锁竞争,提高并发性能。
    • 由于初始化操作通常只需要执行一次(典型的 双重检查锁 模式),CAS 在多数情况下不会失败,所以开销较小。

2. 计算出的位置为空时,使用 CAS 插入

  • 原因 :如果某个桶(即 table[index] )位置为空,说明没有哈希冲突,我们可以直接尝试插入数据。

  • 实现 :使用 CAS 方式直接插入:

  • if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null))) {
        break; // 插入成功,退出循环
    }

    为什么用 CAS 而不是 synchronized ?

  • 因为这个位置是 null ,没有竞争,所以可以直接尝试用 无锁的 CAS 操作 插入,避免加锁的开销,提高性能。

3. 计算出的位置不为空时,使用 synchronized

  • 原因 :如果 table[index] 位置已经有元素了,可能会遇到 哈希冲突 ,需要遍历该链表或红黑树进行替换或追加。

  • 实现

    • 先通过 synchronized 锁住该桶( synchronized (f) )。
    • 然后遍历这个桶:
      • 如果 key 已存在 ,则更新 value。
      • 如果 key 不存在 ,则添加新的节点(链表 or 红黑树)。
    • 插入完成后, 判断链表长度是否达到阈值(8),如果达到就转换为红黑树
  • 为什么用 synchronized 而不是 CAS?

    • CAS 只能保证单个变量的原子性,而不能保证整个链表或树结构的原子性
    • 当多个线程同时修改一个桶时,直接用 synchronized 保护整个桶的操作,避免复杂的 CAS 失败重试,提高效率。

JDK 1.8 在 ConcurrentHashMap 中通过 分阶段使用 CAS 和 synchronized ,既保证了 高并发性能 ,又保证了 线程安全 ,这就是它不同情况下采用不同方式的原因。