乐于分享
好东西不私藏

ConcurrentHashMap 源码解析(JDK8)高并发哈希表的终极实现

ConcurrentHashMap 源码解析(JDK8)高并发哈希表的终极实现

哈喽,各位程序员伙伴~👋

春节已过,继续收心学习。我们继续把 Java 并发底层啃透!

上一篇我们彻底讲清了 ThreadLocal 线程本地存储与内存泄漏。今天我们进入 JUC 最核心、面试最高频的组件:

ConcurrentHashMap(JDK8 版)

它是高并发场景下必用的线程安全 Map,从 JDK7 的分段锁,到 JDK8 的 CAS + synchronized 锁头节点,整个设计堪称并发工程的教科书。

一、先一句话抓住核心(JDK7 vs JDK8)

    1. JDK7

      • 结构:Segment 分段锁 + 数组 + 链表
      • 锁粒度:Segment(粗)
      • 并发度:等于 Segment 个数
    2. JDK8

      • 结构:数组 + 链表 / 红黑树
      • 锁:CAS + synchronized 锁头节点
      • 锁粒度:桶级别(最小化)
      • 并发能力:大幅提升

    一句话总结:JDK8 抛弃分段锁,用更细粒度的锁 + 无锁 CAS,实现真正高并发。

    二、JDK8 ConcurrentHashMap 核心结构

    public class ConcurrentHashMap<K,V> {    // 哈希表数组    transient volatile Node<K,V>[] table;    // 链表转红黑树阈值    static final int TREEIFY_THRESHOLD = 8;    // 红黑树退化为链表阈值    static final int UNTREEIFY_THRESHOLD = 6;    // 树化的最小容量    static final int MIN_TREEIFY_CAPACITY = 64;    // 控制标识:初始化、扩容、计数    transient volatile int sizeCtl;}

    三、核心 Node 节点(JDK8 真实结构)

    static class Node<K,V> implements Map.Entry<K,V> {    final int hash;    final K key;    volatile V val;    volatile Node<K,V> next;    // ...}

      关键点:

      • table
         是 volatile,保证多线程可见。
      • sizeCtl
         是整个 ConcurrentHashMap  的 “指挥中心”。

      四、核心流程:put 方法源码(最精简准确版)

      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;        // 1. 数组为空 → 初始化        if (tab == null || (n = tab.length) == 0)            tab = initTable();        // 2. 对应下标为空 → CAS 无锁插入        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {            if (casTabAt(tab, i, nullnew Node<>(hash, key, value, null)))                break;        }        // 3. 头节点 hash == MOVED → 正在扩容,帮助扩容        else if ((fh = f.hash) == MOVED)            tab = helpTransfer(tab, f);        // 4. 正常哈希冲突 → 锁头节点        else {            V oldVal = null;            synchronized (f) { // 只锁当前桶的头节点!                if (tabAt(tab, i) == f) {                    if (fh >= 0) {                        // 链表插入                    } else {                        // 红黑树插入                    }                }            }        }    }    addCount(1L, binCount);    return oldVal;}

      put 真正逻辑(背会就能吊打面试)

      1. key/value 都不能为 null
      2. 计算 hash(spread 做二次扰动,减少碰撞)
      3. 数组未初始化 → 用 sizeCtl 控制初始化
      4. 目标桶为空 → CAS 无锁插入
      5. 桶头是 FORWARD 节点 → 当前正在扩容,线程协助迁移
      6. 否则:synchronized 锁住头节点,再插入
      7. 链表长度 ≥8 且数组长度≥64 → 转为红黑树
      8. 最后计数,并判断是否需要扩容

      五、为什么 JDK8 这么强?

        • 锁粒度极小

          只锁当前桶的头节点,不同桶完全并行。

        • 无冲突 = 无锁

          大部分写操作直接 CAS 完成,性能接近 HashMap。

        • 扩容多线程协同

          迁移过程分段、分桶,线程之间互不抢锁。

        • 链表 / 红黑树自动切换

          最坏时间复杂度从 O (n) → O (logn),防哈希攻击。

        六、必须搞懂的关键细节(面试高频)

          1. 为什么 key 和 value 不能为 null?

          • HashMap 允许 key/value 为 null
          • ConcurrentHashMap 都不允许

          原因:高并发场景下,get(null) 无法区分是不存在还是值为 null,会带来歧义。为了并发安全,直接禁止 null。

          2. get 方法真・完全无锁

          • 全程不加锁
          • 靠 volatile Node[] table
          • 靠 volatile Node.val
          • 靠 tabAt 原子读取

          只要写入用 volatile 保证可见,读取就可以安全无锁。

          3. sizeCtl 的 4 种含义(超级高频)

          • sizeCtl = -1:正在初始化
          • sizeCtl < -1:正在扩容
          • sizeCtl = 0:未初始化
          • sizeCtl > 0:下一次扩容的阈值(容量 × 0.75)

          它是ConcurrentHashMap 所有并发控制的 “总开关”。

          4. 为什么链表长度 ≥8 才树化?

          • 理想哈希下,链表长度达到 8 的概率极低(泊松分布)
          • 达到 8 基本意味着:哈希攻击 或 极差 hashCode
          • 树化是为了保证最坏情况下的性能:O(logn)

          5. 树化必须满足两个条件

          • 链表长度 ≥ 8
          • 数组长度 ≥ 64

          不满足 64 时,优先扩容而不是树化,目的是减少冲突。

          6. 扩容机制(JDK8 灵魂)

          • 扩容条件:元素数量 ≥ sizeCtl
          • 新容量 = 旧容量 × 2
          • 迁移方式:按桶分配,多线程协同迁移
          • 迁移中:头节点标记为 FORWARD,其他读写线程会协助迁移
          • 迁移完成才切换到新 table

          这是 JDK8 ConcurrentHashMap 高并发的核心。

          7. size () 是弱一致性

          • size()不是实时精确值
          • 高并发下不加全局锁统计,而是累加计数单元
          • 追求并发性能 > 绝对实时准确
          • 正式推荐使用:mappingCount()(返回 long)

          七、JDK7 vs JDK8 一张表总结

          维度
          JDK7
          JDK8
          结构
          Segment + 数组 + 链表
          数组 + 链表 + 红黑树
          锁机制
          ReentrantLock 分段锁
          CAS + synchronized 锁头节点
          锁粒度
          Segment(粗)
          桶头节点(极细)
          并发能力
          中等
          极高
          哈希冲突
          链表
          链表 + 红黑树
          扩容
          单线程迁移
          多线程协同迁移
          性能
          一般
          高并发下远超 JDK7

          八、总结(可直接背)

          • JDK8 ConcurrentHashMap 抛弃分段锁,采用数组 + 链表 + 红黑树。
          • 读完全无锁,写无冲突用 CAS,有冲突只锁桶头。
          • 扩容多线程协同,性能极强。
          • key/value 不能为 null,size 弱一致,get 完全无锁。
          • 高并发场景下:必用 CHM,绝不用 HashMap、Hashtable。

          下一篇预告

          BlockingQueue 全家桶源码解析

          ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue线程池的 “血脉”,面试必问。

          本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » ConcurrentHashMap 源码解析(JDK8)高并发哈希表的终极实现

          评论 抢沙发

          2 + 8 =
          • 昵称 (必填)
          • 邮箱 (必填)
          • 网址
          ×
          订阅图标按钮