Java 8 中的 ConcurrentHashMap 详解

Java 8 对 ConcurrentHashMap 进行了重大优化,摒弃了分段锁(Segment)的设计,转而采用更高效的 CAS(Compare-And-Swap) + synchronized 结合的方式,显著提升了并发性能。以下是其核心原理、实现细节及使用场景的详细解析。


一、核心设计思想

  1. 目标:

    • 在高并发场景下,提供线程安全且高效的哈希表操作。
    • 通过细粒度锁 (仅锁定单个哈希桶)和无锁读volatile 变量)减少竞争。
  2. 关键优化:

    • 链表转红黑树:解决哈希冲突时链表查询效率低的问题。
    • 多线程协同扩容:避免全表锁定的开销。

二、数据结构

1. 核心成员变量

java

java 复制代码
transient volatile Node<K,V>[] table;      // 哈希桶数组
private transient volatile int sizeCtl;  // 控制表初始化和扩容的标记
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;      // 值用 volatile 修饰,保证可见性
    volatile Node<K,V> next; // 链表指针
}
  • 哈希桶数组(table) :每个桶是一个链表或红黑树(TreeBin 节点)。

  • sizeCtl:

    • -1:表正在初始化。
    • -N:有 N-1 个线程正在扩容。
    • 正数:表未初始化时表示初始容量;初始化后表示扩容阈值。
2. 链表转红黑树
  • 触发条件:当链表长度 ≥8 且哈希表容量 ≥64 时,链表转为红黑树。
  • 回退条件:当红黑树节点数 ≤6 时,退化为链表。
  • 目的 :将链表查询的 O(n) 时间复杂度优化为红黑树的 O(log n)
3. TreeBin 节点
scala 复制代码
static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;          // 红黑树根节点
    volatile TreeNode<K,V> first; // 链表头节点(遍历用)
    volatile Thread waiter;      // 等待线程
    volatile int lockState;      // 锁状态(CAS 控制)
}
  • 红黑树的操作通过 TreeBin 封装,内部通过 CASsynchronized 保证线程安全。

三、扩容机制

触发条件 : • 当哈希表中的元素数量超过 容量 × 负载因子(默认 0.75)。 • 单次插入后,若发现当前桶已迁移完毕且需要扩容,触发协作扩容。

扩容流程

  1. 初始化新数组: 新数组大小为原数组的 2 倍(例如从 16 扩容到 32)。

    ini 复制代码
    Node<K,V>[] nextTable = new Node[n << 1];  
  2. 分配迁移任务: 将旧数组的桶分为多个区间,每个线程负责迁移一部分区间。

  3. 多线程协作迁移 : • 步长(stride) :每个线程每次处理一个桶区间(如 16 个桶)。 • 高低位拆分 : 将旧桶中的链表或红黑树节点按哈希值的高低位拆分到新数组的两个位置(ii + n)。

    arduino 复制代码
    // 示例:旧桶索引为 i,新桶索引为 i 或 i + n  
    if ((e.hash & n) == 0) {  
        // 低位链表  
    } else {  
        // 高位链表  
    }  
  4. 迁移完成标志 : 迁移完成后,更新哈希表引用 table = nextTable,并更新 sizeCtl 为新的扩容阈值。

协作扩容 : • 其他线程在插入时若检测到正在扩容(Node.hash == MOVED),会调用 helpTransfer() 协助迁移。 • 源码片段

javascript 复制代码
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {  
    if (tab != null && f instanceof ForwardingNode) {  
        // 协助迁移数据  
    }  
}  

四、删除操作

流程

  1. 定位哈希桶 : 根据键的哈希值找到对应的桶(索引为 (n - 1) & hash)。
  2. 处理链表或红黑树 : • 链表 :遍历链表找到目标节点,修改前驱节点的 next 指针。 • 红黑树 :通过 TreeBinremoveTreeNode() 方法删除节点,并调整树结构。
  3. 原子替换或 CAS 操作 : • 若删除的是头节点,使用 CAS 替换头节点。 • 若删除的是中间节点,需在同步块中操作(通过 synchronized 锁定头节点)。

源码示例(链表删除):

scss 复制代码
synchronized (f) {  
    if (tabAt(tab, i) == f) {  
        // 遍历链表删除节点  
        if (prev != null) {  
            prev.next = e.next;  
        } else {  
            setTabAt(tab, i, e.next);  
        }  
    }  
}  

红黑树删除 : • 调用 TreeBinremoveTreeNode() 方法,平衡红黑树结构。 • 若树节点数 ≤6,将红黑树退化为链表。


五、获取操作

流程

  1. 计算哈希值

    ini 复制代码
    int hash = spread(key.hashCode());  
  2. 定位哈希桶 : 根据哈希值找到对应的桶(索引为 (n - 1) & hash)。

  3. 遍历链表或红黑树 : • 链表 :从头节点开始遍历,匹配 keyhash。 • 红黑树 :调用 TreeBinfind() 方法,在树中搜索节点。

无锁读实现 : • 所有 Node.valNode.next 字段均用 volatile 修饰,保证可见性。 • 红黑树的 TreeBin 通过 volatile 状态标记确保读操作安全。

源码示例

kotlin 复制代码
public V get(Object key) {  
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;  
    int h = spread(key.hashCode());  
    if ((tab = table) != null && (n = tab.length) > 0 &&  
        (e = tabAt(tab, (n - 1) & h)) != null) {  
        if ((eh = e.hash) == h) {  
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))  
                return e.val;  
        } else if (eh < 0) // 红黑树查找  
            return (p = e.find(h, key)) != null ? p.val : null;  
        while ((e = e.next) != null) { // 遍历链表  
            if (e.hash == h && ((ek = e.key) == key || key.equals(ek)))  
                return e.val;  
        }  
    }  
    return null;  
}  

五、链表转红黑树(树化)

触发条件 : • 链表长度 ≥ TREEIFY_THRESHOLD(默认 8)。 • 哈希表容量 ≥ MIN_TREEIFY_CAPACITY(默认 64)。

树化流程

  1. 锁定桶头节点 : 使用 synchronized 锁定头节点,防止并发修改。

  2. 创建 TreeBin : 将链表节点转换为红黑树,并创建 TreeBin 对象作为新的头节点。

    scss 复制代码
    TreeBin<K,V> tb = new TreeBin<K,V>();  
    tb.addAll(head); // 将链表节点添加到红黑树  
    setTabAt(tab, index, tb); // 替换链表为 TreeBin  
  3. 维护双向链表TreeBin 内部维护一个双向链表,支持按插入顺序遍历。

退化为链表 : • 当红黑树节点数 ≤ UNTREEIFY_THRESHOLD(默认 6)时,调用 untreeify() 方法将树转为链表。

源码示例

ini 复制代码
private final void treeifyBin(Node<K,V>[] tab, int index) {  
    Node<K,V> b; int n, sc;  
    if (tab != null) {  
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)  
            tryPresize(n << 1); // 容量不足时扩容  
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {  
            synchronized (b) {  
                // 将链表转为红黑树  
                if (tabAt(tab, index) == b) {  
                    TreeNode<K,V> hd = null, tl = null;  
                    for (Node<K,V> e = b; e != null; e = e.next) {  
                        TreeNode<K,V> p = new TreeNode<>(e.hash, e.key, e.val, null, null);  
                        if ((p.prev = tl) == null)  
                            hd = p;  
                        else  
                            tl.next = p;  
                        tl = p;  
                    }  
                    setTabAt(tab, index, new TreeBin<K,V>(hd));  
                }  
            }  
        }  
    }  
}  

五、线程安全实现

  1. CAS 无锁操作 : • 头节点插入、sizeCtl 修改等通过 CAS 实现无锁化。 • 示例:casTabAt(tab, i, null, new Node(...))
  2. synchronized 锁: • 哈希冲突时锁定桶的头节点,保证链表或树操作的原子性。
  3. volatile 变量 : • table 数组、Node.valNode.next 等字段用 volatile 修饰,保证可见性。

六、对比 Java 7 与 Java 8 的设计

特性 Java 7(分段锁) Java 8(桶锁 + CAS)
锁粒度 段锁(默认 16 段) 桶锁(仅锁定单个桶的头节点)
哈希冲突处理 链表 链表转红黑树(优化查询效率)
扩容机制 单线程扩容 多线程协作扩容
内存开销 高(每个段维护独立哈希表) 低(统一哈希表结构)
读性能 无锁读但需遍历段 无锁读且直接访问目标桶

七、适用场景总结

  1. 高并发读写:如缓存系统、实时计算中间状态存储。
  2. 动态扩容需求:支持多线程协作扩容,避免长时间阻塞。
  3. 复杂哈希冲突:通过红黑树优化长链表的查询效率。

八、注意事项

  1. 避免哈希冲突不均匀:设计良好的哈希函数,防止大量键映射到同一桶。
  2. 合理初始化容量:根据预估数据量设置初始容量,减少扩容次数。
  3. 慎用 size() 和 mappingCount() : • size() 返回的是近似值,不保证精确性。 • mappingCount() 返回 long 类型的更准确计数。
相关推荐
考虑考虑3 小时前
Jpa使用union all
java·spring boot·后端
用户3721574261353 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊4 小时前
Java学习第22天 - 云原生与容器化
java
渣哥6 小时前
原来 Java 里线程安全集合有这么多种
java
间彧6 小时前
Spring Boot集成Spring Security完整指南
java
间彧7 小时前
Spring Secutiy基本原理及工作流程
java
Java水解8 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆10 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学10 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole10 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端