ConcurrentHashMap 实现的原理

ConcurrentHashMap 是 Java 并发包中提供的高性能线程安全的哈希表实现,其设计目标是支持高并发的读写操作。以下是其核心实现原理的详细分析:

分段锁(JDK 1.7 实现)

在 JDK 1.7 中,ConcurrentHashMap 使用分段锁(Segment)机制实现线程安全。哈希表被分为多个 Segment,每个 Segment 是一个独立的哈希表,拥有自己的锁。不同 Segment 的写操作可以并发执行,从而提高并发性能。

  • 数据结构
    ConcurrentHashMap 由多个 Segment 组成,每个 Segment 是一个类似 HashMap 的结构,包含一个 HashEntry 数组。Segment 的数量通过并发级别(concurrencyLevel)指定,默认为 16。

  • 锁粒度
    每个 Segment 继承自 ReentrantLock,写操作只需锁定当前 Segment,读操作无需加锁(通过 volatile 保证可见性)。

  • 哈希冲突处理
    使用链地址法解决冲突,每个 HashEntry 是一个链表节点。

CAS + synchronized(JDK 1.8 实现)

JDK 1.8 对 ConcurrentHashMap 进行了优化,摒弃了分段锁,改用 CAS + synchronized 实现更细粒度的锁控制。

  • 数据结构
    使用 Node 数组 + 链表/红黑树的结构(类似 HashMap)。当链表长度超过阈值(TREEIFY_THRESHOLD默认为 8)会尝试将链表转换为红黑树。仅当当前哈希表的容量大于或等于 MIN_TREEIFY_CAPACITY(默认值为 64)时,链表才会真正转换为红黑树。如果容量不足,会优先触发扩容(resize)操作。当红黑树中的节点数量减少到 UNTREEIFY_THRESHOLD(默认值为 6)时,会将红黑树转换回链表。

  • 线程安全机制

    • 读操作:无锁,直接访问 volatile 的 Node 数组和节点数据。
    • 写操作
      • 使用 CAS 初始化数组或插入节点。
      • 发生冲突时,对链表头节点或树根节点加 synchronized 锁。
    • 扩容:支持多线程协同扩容,通过 ForwardingNode 标记迁移状态。

关键优化点

  • 减小锁粒度
    JDK 1.8 的锁仅针对单个桶(链表头或树根),比分段锁的粒度更细,进一步减少竞争。

  • 红黑树优化
    链表过长时转换为红黑树,将查询时间复杂度从 O(n) 降为 O(log n)。

  • 并发扩容
    扩容时允许其他线程协助迁移数据,避免单线程迁移的性能瓶颈。

示例代码片段

以下是 JDK 1.8 中 put 方法的简化逻辑:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 懒初始化数组
    if ((tab = table) == null || (n = tab.length) == 0)
        tab = initTable();
    // CAS 插入节点
    else if ((p = tabAt(tab, i = (n - 1) & hash)) == null) {
        if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
            break;
    }
    // 处理哈希冲突
    else {
        synchronized (p) {
            if (tabAt(tab, i) == p) {
                if (p instanceof TreeNode)
                    // 红黑树插入
                    ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    // 链表插入
                    for (int binCount = 0; ; ++binCount) {
                        if (p.next == null) {
                            p.next = new Node<K,V>(hash, key, value);
                            if (binCount >= TREEIFY_THRESHOLD - 1)
                                treeifyBin(tab, i);
                            break;
                        }
                        p = p.next;
                    }
                }
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

性能对比

  • JDK 1.7:分段锁适合中等并发场景,但在极高并发下仍可能因锁竞争受限。
  • JDK 1.8:CAS + synchronized 在大多数场景下性能更高,尤其是读多写少时接近无锁性能。

适用场景

  • 高并发读写需求(如缓存)。
  • 需要线程安全且性能优于 Hashtable 或 Collections.synchronizedMap 的场景。

通过上述设计,ConcurrentHashMap 在保证线程安全的同时,极大提升了并发性能。

currentHashMap 扩容原理

CurrentHashMap 的扩容机制是为了在并发环境下高效地处理数据增长,同时保持线程安全性。以下是 JDK 8 中 CurrentHashMap 的扩容原理:


触发条件

扩容通常在以下情况下触发:

  • 插入新元素时,发现当前链表或红黑树的节点数超过阈值(默认为 8,且 table 长度小于 64 时会优先扩容)。
  • table 中元素数量超过容量 × 负载因子(默认 0.75)。

扩容过程

  1. 初始化新 table
    创建一个新 table,大小为原 table 的 2 倍(即 newCap = oldCap << 1),并计算新的扩容阈值(newThr = oldThr << 1)。

  2. 数据迁移
    通过多线程协作完成迁移:

    • 每个线程负责迁移一部分桶(bucket),通过 transferIndex 分配任务范围。
    • 迁移时对原 table 的桶加锁(synchronized),保证线程安全。
  3. 桶迁移逻辑

    • 对于链表:将原链表拆分为高位链和低位链,分别放入新 table 的 ii + oldCap 位置。
    • 对于红黑树:调用 TreeNode.split() 方法拆分为两棵树,若节点数小于等于 6 则退化为链表。
  4. 更新引用
    迁移完成后,将新 table 设置为当前 table,并更新阈值。


并发控制

  • CAS 操作:用于更新 sizeCtltransferIndex 等全局变量。
  • synchronized:迁移时对单个桶加锁,避免阻塞其他线程操作其他桶。
  • 多线程协作:其他线程在插入时若发现正在扩容,会协助迁移。

示例代码片段

// JDK 8 扩容核心代码(简化版)
while (s-- > 0) {
    synchronized (f) {
        if (fh >= 0) {
            // 处理链表
            Node<K,V> ln = null, hn = null;
            for (Node<K,V> e = f; e != null; e = e.next) {
                int h = e.hash;
                if ((h & oldCap) == 0) {
                    ln = new Node<K,V>(h, e.key, e.val, ln);
                } else {
                    hn = new Node<K,V>(h, e.key, e.val, hn);
                }
            }
            setTabAt(nextTab, i, ln);
            setTabAt(nextTab, i + oldCap, hn);
        }
        // 处理红黑树...
    }
}


关键点

  • 高效性:多线程并行迁移减少总耗时。
  • 无阻塞:读操作无需加锁,即使正在扩容也能正常访问。
  • 渐进式:扩容期间新旧 table 共存,直到所有数据迁移完成。

为什么扩容使用2的幂次方?

高效计算桶索引:

使用2的幂大小时,模运算被位与操作取代,位与操作的计算成本更低。
例如,如果数组大小是16(2的4次方),表达式 hash & 15 可以高效地计算索引,等同于 hash % 16。
改善哈希码分布:


在位与操作中使用哈希码的低位有助于实现更均匀的键分布。
简化代码:
使用2的幂大小简化了计算桶索引的代码,使其更为简洁,潜在地更快速。
需要注意的是,虽然2的幂扩容提供了这些优势,但在某些情况下,它可能会导致哈希码的模式与2的幂大小对齐,从而产生更多的碰撞。然而,在实践中,简单性和性能之间的权衡通常倾向于使用2的幂扩容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值