ConcurrentHashMap详解

ConcurrentHashMap详解

ConcurrentHashMap 是 Java 集合框架中的一个线程安全的高性能哈希表实现,广泛用于多线程环境中。它在 JDK 1.5 引入,并在 JDK 1.8 进行了重大优化。以下是对 ConcurrentHashMap 的详细设计分析,涵盖其核心原理、数据结构、并发机制和关键优化点。

1. 核心设计目标

ConcurrentHashMap 的设计目标是在高并发场景下提供高效的线程安全操作,同时尽量减少锁的开销。它与 HashtableCollections.synchronizedMap 的全锁机制不同,采用 分段锁(JDK 1.5~1.7)或 细粒度锁+无锁机制(JDK 1.8+)来提高并发性能。主要特点包括:

  • 高并发性:允许多个线程同时读写,读操作通常无锁,写操作尽量减少锁粒度。
  • 线程安全:保证数据一致性,避免并发修改异常。
  • 高性能:通过优化锁机制和数据结构,减少阻塞和竞争。
  • 无阻塞读:读操作不加锁,依赖内存可见性和 volatile 语义。
  • 动态扩展:支持在运行时动态调整容量。

2. 数据结构

ConcurrentHashMap 的底层数据结构在不同 JDK 版本中有显著变化。

JDK 1.5 ~ 1.7:分段锁(Segment Locking)
  • 基本结构:ConcurrentHashMap 由多个 Segment 组成,每个 Segment 是一个独立的哈希表,包含一个桶数组(table),每个桶存储键值对(Entry)。
  • Segment:Segment 是一个继承自 ReentrantLock 的类,每个 Segment 管理一部分桶(table 的一部分)。默认有 16 个 Segment(concurrencyLevel),可以通过构造方法指定。
  • 桶(Bucket):每个桶是一个链表,存储键值对,解决哈希冲突采用 拉链法
  • 并发机制
    • 读操作:无锁,依赖 volatile 语义保证内存可见性。
    • 写操作:对目标 Segment 加锁,锁粒度是整个 Segment,而不是整个哈希表。
    • 扩容:仅对单个 Segment 扩容,不影响其他 Segment。
  • 缺点
    • Segment 数量固定(构造时指定),无法动态调整。
    • 锁粒度仍然较大(锁定整个 Segment)。
    • 迭代器是弱一致性的,可能不反映最新更新。
JDK 1.8+:Node 数组 + 红黑树 + CAS

JDK 1.8 对 ConcurrentHashMap 进行了大幅优化,摒弃了 Segment 机制,采用更细粒度的锁和无锁操作,性能显著提升。

  • 基本结构
    • 核心是 Node 数组(table),每个元素是一个桶,可能存储:
      • Node:普通链表节点,用于拉链法解决哈希冲突。
      • TreeNode:红黑树节点,当链表长度超过阈值(默认 8)时,链表转为红黑树。
      • ForwardingNode:扩容时的特殊节点,用于标记桶正在迁移。
    • table:存储键值对的数组,初始容量为 16,可动态扩容。
  • 哈希冲突处理
    • 链表:当冲突较少时,采用链表存储(Node 节点)。
    • 红黑树:当链表长度超过 8 且 table 容量大于等于 64 时,转换为红黑树(TreeNode),提高查询效率(从 O(n) 到 O(log n))。
  • 并发控制
    • 读操作:无锁,依赖 volatile 语义(Node 的 val 和 next 字段是 volatile)。
    • 写操作
      • 使用 synchronized 锁锁定单个桶(Node 级别),锁粒度比 Segment 更细。
      • 使用 CAS(Compare-And-Swap) 机制进行无锁更新,例如初始化 table 或插入节点。
    • 扩容:支持并发扩容,多线程协同完成数据迁移。
  • 特殊节点
    • ForwardingNode:标记桶已迁移,读线程遇到时会跳转到新 table。
    • TreeBin:红黑树的根节点,包含对红黑树的加锁操作。

3. 关键操作原理

以下是 ConcurrentHashMap 的核心操作及其实现细节。

初始化
  • table 初始化
    • table 在第一次 put 操作时延迟初始化(lazy initialization)。
    • 使用 CAS 操作(sizeCtl 字段)确保只有一个线程初始化 table。
    • sizeCtl:控制表初始化和扩容的标志,负数表示正在初始化或扩容。
  • 并发级别:JDK 1.8 不再依赖构造方法中的 concurrencyLevel(仅用于向后兼容),而是动态调整锁粒度。
put 操作
  1. 计算哈希:对 key 的 hashCode 进行扰动(spread 方法),减少哈希冲突。
  2. 定位桶:通过 (n-1) & hash 计算桶索引。
  3. 插入逻辑
    • 如果桶为空,使用 CAS 插入新节点。
    • 如果桶不为空:
      • 加锁(synchronized)当前桶的头节点。
      • 如果是链表,遍历插入或更新。
      • 如果是红黑树,调用红黑树插入逻辑。
    • 如果链表长度超过 8,触发树化(treeifyBin)。
  4. 扩容检查
    • 插入后检查 size 是否超过阈值(loadFactor * capacity)。
    • 如果需要扩容,触发 transfer 操作。
get 操作
  • 无锁操作,依赖 volatile 语义。
  • 过程:
    1. 计算哈希,定位桶。
    2. 如果桶头节点匹配,直接返回。
    3. 如果是链表,遍历查找。
    4. 如果是红黑树,调用红黑树查找逻辑。
    5. 如果遇到 ForwardingNode,跳转到新 table 查找。
扩容(resize)
  • 触发条件
    • 插入新键值对后,size 超过阈值。
    • 链表长度过长(转为红黑树前可能触发扩容)。
  • 过程
    1. 创建新 table(通常容量翻倍)。
    2. 多线程协同迁移:
      • 每个线程领取一段桶区间(transferIndex 控制)。
      • 使用 ForwardingNode 标记已迁移的桶。
      • 迁移时对单个桶加锁(synchronized)。
    3. 迁移完成后,更新 table 引用。
  • 优化
    • 多线程并发迁移,分担工作量。
    • 读线程在扩容期间仍可访问旧表或新表,保持一致性。
size 操作
  • 计数机制
    • 使用 CounterCell 数组(类似 LongAdder)记录增量更新,分散计数热点。
    • 最终 size 通过 sumCount 方法汇总。
  • 弱一致性:size 不保证实时精确,适合高并发场景。

4. 并发优化

ConcurrentHashMap 的高性能源于以下优化:

  • 细粒度锁:JDK 1.8 使用 synchronized 锁定单个桶,取代 Segment 锁。
  • CAS 操作:初始化、插入、计数等操作大量使用 CAS,避免锁竞争。
  • 无锁读:读操作不加锁,依赖 volatile 保证可见性。
  • 红黑树:当哈希冲突严重时,链表转为红黑树,提升查询效率。
  • 并发扩容:多线程协同迁移,分担扩容开销。
  • CounterCell:分散计数,避免单点竞争。

5. 关键字段和参数

  • table:存储键值对的数组,volatile 保证可见性。
  • sizeCtl
    • 正数:表示初始化容量或扩容阈值。
    • -1:表示正在初始化。
    • 负数(-(1 + n)):表示 n 个线程正在扩容。
  • loadFactor:默认 0.75,控制扩容时机。
  • TREEIFY_THRESHOLD:默认 8,链表转为红黑树的阈值。
  • UNTREEIFY_THRESHOLD:默认 6,红黑树退化为链表的阈值。
  • MIN_TRANSFER_STRIDE:默认 16,扩容时每个线程处理的最小桶数。

6. 与 HashMap 的区别

  • 线程安全
    • HashMap 非线程安全,可能在并发修改时导致死循环或数据丢失。
    • ConcurrentHashMap 线程安全,适合多线程环境。
  • 锁机制
    • HashMap 无锁。
    • ConcurrentHashMap 使用 CAS 和细粒度锁。
  • 性能
    • HashMap 单线程性能略高(无同步开销)。
    • ConcurrentHashMap 在多线程场景下性能优于 Hashtable 和 synchronizedMap。
  • 迭代器
    • HashMap 的迭代器是 fail-fast,并发修改抛出 ConcurrentModificationException。
    • ConcurrentHashMap 的迭代器是弱一致性,允许并发修改。

7. 适用场景

  • 高并发读写:如缓存、配置管理、计数器等。
  • 需要线程安全但不想全锁:比 Hashtable 和 synchronizedMap 更高效。
  • 大规模数据:红黑树优化适合大数据量场景。

8. 源码关键点

以下是一些关键方法的伪代码(以 JDK 1.8 为例):

putVal
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break; // 无锁插入
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f); // 协助扩容
        else {
            V oldVal = null;
            synchronized (f) { // 锁定桶头节点
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { // 链表
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash && ((ek = e.key) == key || key.equals(ek))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) { // 红黑树
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount); // 更新计数,可能触发扩容
    return null;
}
get
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, h;
    if (key == null) throw new NullPointerException();
    h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if (e.hash == h && ((k = e.key) == key || key.equals(k)))
            return e.val;
        if (e.hash >= 0) { // 链表
            while (e != null) {
                if (e.hash == h && ((k = e.key) == key || key.equals(k)))
                    return e.val;
                e = e.next;
            }
        }
        else if (e instanceof TreeBin) // 红黑树
            return ((TreeBin<K,V>)e).getTreeNode(h, key).val;
        else if (e.hash == MOVED) // 扩容中
            return forwarderGet(e, key);
    }
    return null;
}

9. 性能分析

  • 时间复杂度
    • 读操作:平均 O(1)(链表),最差 O(log n)(红黑树)。
    • 写操作:平均 O(1)(单桶操作),扩容时 O(n)。
  • 空间复杂度:O(n),n 为键值对数量。
  • 并发性能
    • JDK 1.7:受限于 Segment 数量(默认 16)。
    • JDK 1.8:桶级锁 + CAS,理论上支持更高并发。

10. 注意事项

  • 键值非空:ConcurrentHashMap 不允许 null 键或值。
  • 弱一致性:迭代器、size 等操作不保证实时一致。
  • 扩容影响:高并发下频繁扩容可能导致性能抖动。
  • 序列化:ConcurrentHashMap 支持序列化,但需注意并发状态可能不完全保存。

11. 总结

ConcurrentHashMap 是 Java 并发编程中高效、线程安全的哈希表实现。JDK 1.8 通过引入红黑树、细粒度锁、CAS 和并发扩容,显著提升了性能和可扩展性。其设计精髓在于 分而治之无锁优化,适合高并发场景下的键值存储需求。理解其内部机制(如 CAS、桶级锁、红黑树转换)有助于开发者更好地使用和优化并发程序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值