ConcurrentHashMap:用 “社区超市” 故事讲透并发的设计哲学

"好的架构都能在生活中找到原型"。ConcurrentHashMap(简称 CHM)作为 Java 并发容器的 "扛把子",其设计思想本质就是解决 "多个人同时用一个东西还不乱" 的问题 ------ 这和社区超市处理 "多顾客同时购物" 的逻辑如出一辙。今天咱们就用 "小 A 的社区超市" 故事,结合代码和时序图,把 CHM 的原理扒得明明白白。

一、故事开篇:为什么需要 CHM?(并发容器的痛点)

小 A 开了家社区超市,初期只有 1 个收银台(对应HashMap):

  • 平时人少的时候,顾客拿东西、结账都很快;

  • 一到下班高峰期,所有顾客都挤在一个收银台(多线程竞争同一把锁),后面的人全堵着(线程阻塞),甚至有人抢着付钱导致账单算错(数据不一致)。

小 A 试过把收银台换成 "全店锁"(对应Hashtable):只要有人在结账,全店其他人都不能动 ------ 虽然账单不会错,但效率低到顾客投诉,显然不行。

这时小 A 悟了:问题的核心是 "锁的粒度太粗" 。要让多个人同时操作,又不冲突,就得把 "大锁" 拆成 "小锁",让不同区域的操作互不干扰。这就是 CHM 的核心设计思想: "分而治之" 的并发控制

二、CHM 的两代进化:从 "分区收银" 到 "智能货架"

CHM 的设计分两个关键版本(JDK1.7 和 JDK1.8),就像超市的两次重大升级,咱们逐个拆解。

1. JDK1.7:分区收银模式(Segment 分段锁)

小 A 的第一次升级:把超市分成 3 个独立区域(零食区、生鲜区、日用品区),每个区域配 1 个收银台(对应Segment)。规则是:

  • 顾客买零食,只去零食区收银台,不影响生鲜区的人结账;

  • 同一区域的顾客还是要排队(同一 Segment 内的线程竞争锁),但不同区域可并行。

这就是 JDK1.7 CHM 的核心 ------Segment 分段锁

1.1 结构类比:超市分区 = CHM 的 Segment 数组

先看 JDK1.7 CHM 的核心结构(代码简化版):

java 复制代码
public class ConcurrentHashMap<K, V> {
    // 核心:Segment数组(每个Segment就是一个"分区收银台")
    private final Segment<K, V>[] segments;

    // Segment本质是个"小HashMap",且自带锁(继承ReentrantLock)
    static final class Segment<K, V> extends ReentrantLock implements Serializable {
        // 每个Segment内部有自己的HashEntry数组(对应分区内的货架)
        transient volatile HashEntry<K, V>[] table;
        // 分区内的元素数量
        transient int count;
    }

    // 每个HashEntry就是货架上的"商品格子"(存储key-value)
    static final class HashEntry<K, V> {
        final int hash;
        final K key;
        // value用volatile修饰:保证多线程可见性(不用锁也能读对)
        volatile V value;
        // 链表:解决哈希冲突(同一货架摆多件商品)
        volatile HashEntry<K, V> next;
    }
}

结构对应关系:

CHM 组件 超市场景 作用
segments数组 3 个购物区域 拆分锁的粒度,实现分区并行
Segment 区域收银台 + 货架 自带锁,控制区域内并发
HashEntry[] 区域内的货架 存储具体数据(商品)
HashEntry 货架上的商品格子 存 key-value,链表解决冲突

1.2 核心操作:put(顾客结账)的流程

顾客买了一袋薯片(key = 薯片,value=5 元),结账流程对应 CHM 的put方法:

java 复制代码
public V put(K key, V value) {
    // 1. 计算key的哈希值,确定要去哪个Segment(比如哈希=1→零食区)
    int hash = hash(key);
    int segmentIndex = (hash >>> segmentShift) & segmentMask;

    // 2. 拿到目标Segment,加锁(顾客去零食区收银台,锁住收银台)
    Segment<K, V> s = segments[segmentIndex];
    s.lock(); // 继承ReentrantLock的锁方法
    try {
        // 3. 在Segment内部的HashEntry数组中找位置(货架找格子)
        HashEntry<K, V>[] tab = s.table;
        int index = (hash >>> tabShift) & tabMask;
        HashEntry<K, V> e = tab[index];

        // 4. 处理冲突:如果格子已有商品,遍历链表(同一货架摆多件)
        if (e == null) {
            // 格子空,直接放新商品
            tab[index] = new HashEntry<>(hash, key, value, null);
        } else {
            V oldValue;
            // 遍历链表,找到相同key就更新value,否则加在链表末尾
            for (;;) {
                if (e.hash == hash && e.key.equals(key)) {
                    oldValue = e.value;
                    e.value = value; // 更新价格
                    break;
                }
                if (e.next == null) {
                    e.next = new HashEntry<>(hash, key, value, null); // 新增商品
                    break;
                }
                e = e.next;
            }
        }
        // 5. 更新分区内的商品数量
        s.count++;
        return null;
    } finally {
        // 6. 解锁(结账完成,释放收银台)
        s.unlock();
    }
}

关键亮点

  • 锁只加在Segment上,其他Segment不受影响(零食区锁了,生鲜区照样用);
  • HashEntry.valuevolatile修饰:get方法不用锁就能读到最新值(顾客拿商品看价格,不用等收银台解锁)。

1.3 JDK1.7 的痛点:分区还是不够细

随着超市生意变好,每个区域的货架越来越长(HashEntry链表变长):

  • 顾客找商品要遍历长链表(查询效率 O (n));

  • 同一区域的顾客还是要排队(比如零食区人多,收银台依然拥堵)。

小 A 意识到:锁的粒度还能再细 ------ 从 "区域锁" 降到 "货架锁" 。这就催生了 JDK1.8 的升级。

2. JDK1.8:智能货架模式(CAS+synchronized + 红黑树)

小 A 的第二次升级:

  1. 去掉分区收银台,改成 "自助结账"(无锁 / CAS);

  2. 每个货架配一个 "临时锁"(synchronized):只有人在这个货架拿东西时,才锁这个货架,其他货架正常用;

  3. 把超过 8 件商品的长货架,换成 "智能旋转货架"(红黑树):找商品从 "逐个翻" 变成 "按编号定位"(查询效率从 O (n)→O (logn))。

这就是 JDK1.8 CHM 的核心:锁粒度降到桶级(货架)+ CAS 无锁操作 + 红黑树优化

2.1 结构类比:智能货架 = CHM 的 Node 数组

JDK1.8 彻底抛弃了Segment,核心结构简化为 "Node 数组"(对应货架数组),冲突时用链表 + 红黑树存储:

java 复制代码
public class ConcurrentHashMap<K, V> {
    // 核心:Node数组(每个Node就是一个"货架")
    transient volatile Node<K, V>[] table;

    // 扩容时用的临时数组(相当于临时货架)
    private transient volatile Node<K, V>[] nextTable;

    // 基本节点:对应货架上的"商品格子"
    static class Node<K, V> implements Map.Entry<K, V> {
        final int hash;
        final K key;
        // value和next都用volatile修饰:保证可见性
        volatile V value;
        volatile Node<K, V> next;
    }

    // 红黑树节点:长链表(>8)转红黑树时用
    static final class TreeNode<K, V> extends Node<K, V> {
        TreeNode<K, V> parent; // 父节点
        TreeNode<K, V> left;   // 左子树
        TreeNode<K, V> right;  // 右子树
        boolean red;           // 红黑树颜色标记
    }

    // 扩容标记节点:表示当前货架正在扩容
    static final class MoveableNode<K, V> extends Node<K, V> {
        Node<K, V>[] nextTable;
        MoveableNode(Node<K, V>[] tab) {
            super(-1, null, null, null); // hash=-1标记为MOVED
            this.nextTable = tab;
        }
    }
}

结构对应关系:

CHM 组件 超市场景 作用
table(Node[]) 智能货架数组 每个货架独立锁,粒度更细
Node 货架格子 存 key-value,链表解决冲突
TreeNode 智能旋转货架 长链表优化,提升查询效率
MoveableNode 货架扩容标记 通知其他顾客帮忙扩容

2.2 核心操作:put(顾客自助结账)的流程

顾客买了一盒牛奶(key = 牛奶,value=6 元),自助结账流程对应 JDK1.8 CHM 的put方法(代码简化版):

java 复制代码
public V put(K key, V value) {
    return putVal(key, value, false);
}

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 1. 两次哈希:减少哈希冲突(相当于给商品编一个更唯一的货架号)
    int hash = spread(key.hashCode());
    int binCount = 0;

    // 遍历Node数组(货架数组),自旋处理(没成功就重试)
    for (Node<K, V>[] tab = table;;) {
        Node<K, V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0) {
            // 2. 货架数组没初始化,先初始化(懒加载:顾客来才摆货架)
            tab = initTable();
        } 
        // 3. 定位到目标货架(i = 货架编号),如果货架为空
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 用CAS无锁插入:先看货架是不是空的,是空就放进去(不用等锁)
            if (casTabAt(tab, i, null, new Node<>(hash, key, value, null))) {
                break; // 插入成功,退出循环
            }
        } 
        // 4. 如果货架上有"扩容标记"(MoveableNode),帮忙扩容
        else if ((fh = f.hash) == MOVED) {
            tab = helpTransfer(tab, f); // 顾客帮忙搬货架,加速扩容
        } 
        // 5. 货架有商品,加锁(只锁当前货架的头节点)
        else {
            V oldValue = null;
            synchronized (f) { // 锁货架头节点,其他货架不受影响
                // 再次确认货架没被修改(防止并发修改)
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { // 货架是链表结构
                        binCount = 1;
                        // 遍历链表,找key或插新节点
                        for (Node<K, V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                oldValue = e.value;
                                if (!onlyIfAbsent) {
                                    e.value = value; // 更新value
                                }
                                break;
                            }
                            Node<K, V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<>(hash, key, value, null); // 插链表末尾
                                break;
                            }
                        }
                    } 
                    else if (f instanceof TreeNode) { // 货架是红黑树结构
                        // 红黑树插入(按红黑树规则旋转,保证平衡)
                        Node<K, V> p = ((TreeNode<K, V>)f).putTreeVal(this, tab, hash, key, value);
                        if (p != null) {
                            oldValue = p.value;
                            if (!onlyIfAbsent) {
                                p.value = value;
                            }
                        }
                    }
                }
            }
            // 6. 检查链表长度,超过8就转红黑树(长货架换智能货架)
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD) {
                    treeifyBin(tab, i);
                }
                if (oldValue != null) {
                    return oldValue;
                }
                break;
            }
        }
    }
    // 7. 统计元素数量,触发扩容(商品太多,加货架)
    addCount(1L, binCount);
    return null;
}

JDK1.8 的核心优化点

  1. 锁粒度最小化:从 "分区锁"→"货架锁"(只锁单个桶的头节点),多线程操作不同桶完全并行;
  2. CAS 无锁操作:空桶插入用 CAS(不用加锁),减少锁竞争;
  3. 红黑树优化:链表长度 > 8 转红黑树,查询效率从 O (n)→O (logn);
  4. 多线程扩容 :看到MOVED节点的线程会帮忙扩容,加速过程;
  5. 懒加载 :数组初始化延迟到第一次put,节省内存。

三、时序图:直观看懂 CHM 的调用流程

用时序图(Mermaid 语法)展示 JDK1.7 和 JDK1.8 的put流程,更清晰看到差异。

1. JDK1.7 ConcurrentHashMap put 时序图

2. JDK1.8 ConcurrentHashMap put 时序图

四、总结:CHM 的设计思想精髓

从 "超市故事" 到代码实现,CHM 的设计始终围绕 **"并发安全" 与 "性能高效" 的平衡 **,核心思想可归纳为 3 点:

  1. 分而治之:锁粒度的持续细化

    Hashtable的 "全局锁"→JDK1.7 的 "分段锁"→JDK1.8 的 "桶级锁",每一次细化都让并行度更高,冲突更少。

  2. 无锁 + 有锁:灵活的并发控制

    用 CAS 处理 "无冲突场景"(空桶插入),用synchronized处理 "有冲突场景"(桶内节点操作),既避免了锁的开销,又保证了冲突时的安全。

  3. 数据结构优化:为性能兜底

    用 "链表 + 红黑树" 解决哈希冲突的效率问题,让查询从 "线性遍历" 变成 "对数查找",即使数据量大也不卡顿。

在 Android 开发中,CHM 常用于多线程缓存(如内存缓存LruCache的线程安全改造)、线程池参数存储等场景。理解它的设计思想,不仅能用好这个容器,更能学会 "如何在高并发场景下平衡安全与性能" 的架构思维。

相关推荐
4Forsee2 小时前
【Android】View 交互的事件处理机制
android·交互
龙腾-虎跃2 小时前
Android Sip电话(PJSP)
android
zhangphil2 小时前
Android点击桌面图库应用启动流程trace分析
android
咖啡の猫3 小时前
Android开发-文本输入
android·gitee
No Silver Bullet4 小时前
Android Studio如何开启离线编译模式
android
vocal5 小时前
【Android 构建优化】R8 : 混淆,压缩与优化
android
叽哥6 小时前
Kotlin学习第 7 课:Kotlin 空安全:解决空指针问题的核心机制
android·java·kotlin
诺诺Okami6 小时前
Android Framework-Launcher-默认布局的加载
android
狂浪天涯6 小时前
Android Security | SEAndroid 的主体
android