10-深入理解ConcurrentHashMap(JDK1.8)

1、什么是ConcurrentHashMap

ConcurrentHashMap是java的并发容器,线程安全的HashMap,解决原生HashMap并发死链、数据丢失,以及解决HashTable全局锁性能低下的问题,适用于多线程并发读写场景。

2、为什么需要ConcurrentHashMap

2.1 HashMap 的线程安全问题

HashMap 是非线程安全的。多线程环境下,同时进行 put 操作可能导致死循环(JDK 1.7 头插法)、数据丢失或脏读。即便在 JDK 1.8 中改为尾插法,resize 期间仍可能出现数据不一致。

2.2 Hashtable 的局限性

Hashtable 虽然线程安全,但所有方法都用 synchronized 修饰,相当于给整个对象加锁。高并发下所有操作串行执行,性能急剧下降,无法发挥多核 CPU 的优势。

2.3 ConcurrentHashMap 的登场

ConcurrentHashMap 自 JDK 1.5 引入,经过 JDK 1.8 的重构,采用 CAS + synchronized 的细粒度锁机制,在保证线程安全的同时,将锁竞争降到最低,成为并发场景下 Map 的首选实现。

3、底层数据结构(JDK1.8+)

3.1、数据结构

ConcurrentHashMap的数据结构采用的数组+链表/红黑树。采用数组可以实现随机访问对应的数组下标数据,为了解决哈希冲突问题,使用了链表法,为了防止链表长度过长导致插入/查询性能下降进而增加了红黑树的存储方式。

  • 数组:Node<K, V> table,哈希桶数组,延迟初始化(首次插入时创建)
  • 链表:当哈希较少冲突时,采用单向链表存储节点
  • 红黑树:当链表长度超过阈值默认8且数组长度>=64时,链表转化为红黑树,提高检索效率

链表上的数字都是经过计算的:

java 复制代码
public class MapExample {

    public static void main(String[] args) {
        System.out.println(5 & 15);  // 5
        System.out.println(21 & 15); // 5
        System.out.println(37 & 15); // 5
        System.out.println(53 & 15); // 5
        System.out.println(69 & 15); // 5
        System.out.println(85 & 15); // 5

        System.out.println(8 & 15); // 8
        System.out.println(24 & 15); // 8
        System.out.println(40 & 15); // 8
        System.out.println(56 & 15); // 8
        System.out.println(72 & 15); // 8
        System.out.println(88 & 15); // 8
        System.out.println(104 & 15); // 8
        System.out.println(120 & 15); // 8
        System.out.println(136 & 15); // 8
    }

}

3.2、核心成员变量

java 复制代码
​transient volatile Node<K,V>[] table;          // 哈希桶数组
private transient volatile Node<K,V>[] nextTable; // 扩容时的新数组
private transient volatile long baseCount;     // 基础计数器
private transient volatile int sizeCtl;        // 控制标识

3.3、sizeCtl状态(重要)

sizeCtl=0,tab数组还未创建

sizeCtl>0, 数组初始化完成, 扩容阈值16*0.75=12

sizeCtl=-1, 正在初始化tab数组

sizeCtl<0 不等于-1, 正在扩容,

高16位:`resizeStamp(tab.length)`,本轮扩容的标记
​ 低16位: n+1, n=当前帮助扩容的线程数

​ 有线程加入扩容+1, 线程退出扩容-1,sizeCtl恢复为正数,代替扩容结束,新的扩容阈值

4、源码分析

4.1、put

  • tab如果未初始化,则先初始化
  • 计算key的索引下标
  • 第一:对应的数组为空则直接放入
  • 第二:当前桶处于已迁移状态,参与扩容(符合扩容条件),高低位扩容
  • 第三:不为空,在当前槽位增加synchronized加锁
  • 如果是链表则在尾部加入节点,是否触发转化红黑树
  • 如果是红黑树也将节点加入)

4.1.1、putVal(主方法)

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

指定槽位加锁synchronized保证安全,锁粒度小,并发性能好

java 复制代码
/**
 * ConcurrentHashMap 的 put 核心方法实现
 * 
 * @param key           键
 * @param value         值
 * @param onlyIfAbsent  如果为 true,只有在该 key 不存在时才插入(即 putIfAbsent 逻辑)
 * @return 被覆盖的旧值,如果没有旧值则返回 null
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 1. 前置校验:ConcurrentHashMap 不允许 key 或 value 为 null(区别于 HashMap)
    if (key == null || value == null) throw new NullPointerException();
    
    // 2. 对 key.hashCode() 进行扰动计算(高低位异或),减少哈希冲突
    int hash = spread(key.hashCode());
    
    // 3. binCount 用来记录桶(bin)中的元素个数(链表长度)
    //    用于后续判断是否需要将链表转换为红黑树
    int binCount = 0;
    
    // 4. 自旋(死循环),直到插入操作成功为止
    //    这是典型的 CAS 乐观锁重试机制
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        
        // 5. 如果哈希表还未初始化(延迟加载),则调用 initTable 进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        
        // 6. 根据哈希值定位桶位 (n - 1) & hash,并取出该桶的首节点 f
        //    如果桶为空(f == null),说明没有发生哈希冲突
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 7. 桶为空时,创建新节点,通过 CAS(原子比较并交换)尝试放入该桶
            //    如果 CAS 成功,break 跳出循环;如果失败,说明有其他线程抢先插入了,进入下一轮自旋重试
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // 这里没有加锁,是性能最高的无锁操作
        }
        // 8. 如果桶的首节点 f 的 hash 值为 MOVED(即 -1)
        //    说明当前 table 正在扩容,且该桶已经被迁移(节点类型为 ForwardingNode)
        else if ((fh = f.hash) == MOVED)
            // 当前线程不阻塞,反而主动帮助(helpTransfer)扩容,加快迁移速度
            // 扩容完成后,自旋继续执行插入
            tab = helpTransfer(tab, f);
        
        // 9. 桶不为空,且没有处于扩容状态(即正常写冲突情况)
        else {
            V oldVal = null;
            // 10. 细粒度锁:锁定该桶的首节点 f(而不是锁定整个 table)
            //     不同的桶可以同时被不同线程修改,互不影响
            synchronized (f) {
                // 11. 双重检查(Double-check):加锁成功后,再次确认首节点没有被其他线程改变
                if (tabAt(tab, i) == f) {
                    
                    // 12. 分支一:如果 fh >= 0,说明 f 是一个普通链表节点(非树节点,也非转发节点)
                    if (fh >= 0) {
                        binCount = 1; // 链表长度计数初始为 1
                        // 13. 遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 14. 查找当前 key 是否存在(hash 相等,且 key 对象相等或 equals 相等)
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val; // 记录旧值
                                // 15. 如果 onlyIfAbsent 为 false(普通 put),则覆盖旧值
                                //     如果为 true(putIfAbsent),则保留旧值,不覆盖
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break; // 找到并操作完毕,跳出遍历
                            }
                            // 16. 未找到匹配的 key,继续遍历下一个节点
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                // 17. 遍历到链表尾部,说明 key 不存在,直接尾插法创建新节点
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break; // 插入完毕,跳出遍历
                            }
                        }
                    }
                    // 18. 分支二:如果 f 是 TreeBin 类型(红黑树的根节点包装类)
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2; // 树结构只需设为 2(大于 0 即可,且不会触发转树逻辑)
                        // 19. 调用红黑树的插入方法 putTreeVal,如果返回 null 表示成功插入新节点
                        //     如果返回非 null,表示返回的是已存在的节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val; // 记录旧值
                            // 20. 同样根据 onlyIfAbsent 决定是否覆盖旧值
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            } // synchronized 锁释放(锁的是首节点 f)
            
            // 21. 后续处理(只要 binCount 不等于 0,说明上面的插入/更新操作已执行)
            if (binCount != 0) {
                // 22. 如果链表长度(binCount)大于等于树化阈值(默认 8)
                //     尝试将链表转换为红黑树(注意:treeifyBin 内部还会判断数组长度是否 >= 64)
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 23. 如果 oldVal 不为 null,说明是更新(覆盖)操作,直接返回旧值
                if (oldVal != null)
                    return oldVal;
                break; // 如果是插入新节点(无旧值),跳出主循环
            }
        }
    } // 主循环结束(CAS 插入成功 或 synchronized 插入/更新完毕)
    
    // 24. addCount:更新 Map 中的元素总数(baseCount + CounterCell)
    //     内部会检查是否需要扩容,如果需要则触发扩容或协助扩容
    addCount(1L, binCount);
    return null; // 插入新节点时返回 null
}

4.1.2、协助扩容

java 复制代码
/**
 * 协助数据迁移(扩容)。
 * 当线程在 put 或 compute 等操作中发现某个桶的节点类型为 ForwardingNode(hash == MOVED)时,
 * 意味着当前 table 正在扩容,该线程不会阻塞等待,而是调用此方法主动参与迁移,
 * 充分利用多核 CPU 资源,加速扩容过程。
 *
 * @param tab 当前主哈希表数组(扩容前的旧 table)
 * @param f   当前桶中的首节点,类型必须为 ForwardingNode(转发节点)
 * @return 扩容后的新哈希表数组(nextTable),方便调用方继续后续操作
 */
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    // 1. 声明局部变量:nextTab 指向新表,sc 为 sizeCtl 控制值
    Node<K,V>[] nextTab; int sc;
    
    // 2. 多重校验:确保旧表不为空,且当前桶首节点确实是 ForwardingNode 类型,
    //    并且能从该转发节点中获取到新表(nextTable)的引用
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        
        // 3. 计算扩容批次戳(resizeStamp)并左移 RESIZE_STAMP_SHIFT 位
        //    resizeStamp(tab.length) 会生成一个与当前数组容量(n)相关的唯一标识戳,
        //    左移 16 位(RESIZE_STAMP_SHIFT = 16)后,用于在 sizeCtl 的高位存储扩容标记,
        //    这样 sizeCtl 的负值(高位为 1)既表示正在扩容,又包含了扩容的唯一批次信息。
        int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
        
        // 4. 自旋(循环判断),用于 CAS 竞争加入扩容任务
        //    条件必须同时满足:
        //    - nextTab == nextTable:确保新表没有被其他线程替换
        //    - table == tab:确保旧表没有被重新初始化
        //    - (sc = sizeCtl) < 0:确保扩容仍在进行中(sizeCtl 为负表示扩容中)
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            
            // 5. 判断是否不需要当前线程参与扩容,满足以下任一条件则直接 break 退出循环:
            //    a) sc == rs + MAX_RESIZERS:表示参与扩容的线程数已达上限(MAX_RESIZERS - 1)
            //    b) sc == rs + 1:表示扩容任务已接近尾声(只剩最后一个辅助线程,即发起扩容的线程),
            //                     或者表示没有待迁移的桶了(transferIndex <= 0 会触发此判断)
            //    c) transferIndex <= 0:表示所有待迁移的桶都已经被分配完毕
            if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                transferIndex <= 0)
                break;
            
            // 6. 尝试使用 CAS 将 sizeCtl 的值增加 1(即 sc + 1)
            //    这个操作表示当前线程申请加入扩容工作,使活跃的扩容线程数 +1
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                // 7. CAS 成功后,调用 transfer(tab, nextTab) 执行实质性的数据迁移工作
                //    transfer 方法会将旧 table 中的节点重新哈希到新 table 中,
                //    并且每个线程负责迁移一块连续范围的桶(通过 transferIndex 划分任务)
                transfer(tab, nextTab);
                break; // 迁移完成(或当前线程负责的部分完成),跳出循环
            }
            // 8. 如果 CAS 失败,说明 sizeCtl 被其他线程抢先修改了,
            //    则继续 while 循环重新读取 sc,再次尝试竞争加入扩容
        }
        // 9. 返回新表引用(nextTab),调用方拿到新表后可以继续执行插入等操作
        return nextTab;
    }
    // 10. 如果前置校验不通过(例如当前没有扩容,或节点不是 ForwardingNode),
    //     则直接返回旧表(table),调用方会按照非扩容逻辑继续处理
    return table;
}

触发真实的扩容逻辑:

1、先初始化新表 nextTab

2、一个迁移的线程负责一段数组区间,互不干扰

使用transferIndex默认为数组长度,一个线程负责的区间stride(最小为16);

transferIndex<=0则不能在增加线程来参与扩容了

举例比如:数组长度为64,区间为16

线程一负责区间:48 64
线程二负责区间:32 48
线程三负责区间:16 32
线程四负责区间:0 16

3、迁移

如果桶为空,直接设置成ForwardingNode

如果桶已经被迁移走,跳过

如果桶非空,桶首节点增加synchronized锁,迁移链表/红黑树

java 复制代码
/**
 * 将旧表(tab)中每个桶的节点迁移(或复制)到新表(nextTab)。
 * 这是扩容的核心工作线程(可能多个并发执行)所执行的方法。
 *
 * @param tab     旧表数组
 * @param nextTab 新表数组(可能为 null,若为 null 则由当前线程负责初始化)
 */
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    // 1. 计算每个线程负责的桶区间步长(stride)
    int n = tab.length, stride;
    // 如果 CPU 核心数 > 1,步长 = n / 8 / NCPU,否则步长 = n
    // 保证最小步长为 MIN_TRANSFER_STRIDE(默认 16),避免过度细碎
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // 最小为 16

    // 2. 初始化新表(如果还未创建)
    if (nextTab == null) {            // initiating  ------ 当前线程是首个发起扩容的线程
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 容量翻倍
            nextTab = nt;
        } catch (Throwable ex) {      // 如果 OOM,则将 sizeCtl 设为最大,不再尝试扩容
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;          // 将新表赋给成员变量 nextTable(volatile 可见)
        transferIndex = n;            // 全局迁移索引初始为 n(表示旧表长度,从后往前分配任务)
    }

    int nextn = nextTab.length;           // 新表长度
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 预备转发节点(指向新表)
    boolean advance = true;               // 表示当前线程能否继续向前推进(分配下一个待迁移桶)
    boolean finishing = false;            // 是否所有迁移工作都已结束,用于提交前二次检查

    // 3. 主循环:遍历旧表所有桶,逐个迁移
    //    i 表示当前处理的桶索引,bound 表示当前线程负责区间的下界
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;

        // 4. 分配迁移任务(决定下一个要处理的桶索引 i)
        while (advance) {
            int nextIndex, nextBound;
            // 如果当前桶索引 i 还未到达下界 bound,或者已经完成全部迁移(finishing),则无需再分配
            if (--i >= bound || finishing)
                advance = false;
            // 否则,尝试从全局 transferIndex 中获取一个新区间
            else if ((nextIndex = transferIndex) <= 0) {
                // 如果 transferIndex <= 0,说明所有桶都已被分配完,设置 i = -1 表示无任务
                i = -1;
                advance = false;
            }
            // CAS 抢占下一个区间 [nextBound, nextIndex)
            else if (U.compareAndSwapInt
                     (this, TRANSFERINDEX, nextIndex,
                      nextBound = (nextIndex > stride ?
                                   nextIndex - stride : 0))) {
                bound = nextBound;          // 本线程负责的下界
                i = nextIndex - 1;          // 本线程开始处理的桶索引(从高到低)
                advance = false;            // 任务分配完毕,退出 while 循环,开始迁移
            }
        }

        // 5. 检查是否所有桶都已处理完毕(i 越界)
        if (i < 0 || i >= n || i + n >= nextn) {
            int sc;
            // 如果 finishing 为 true,说明所有线程都已经完成了迁移,可以进行最终提交
            if (finishing) {
                nextTable = null;          // 清除临时引用
                table = nextTab;           // 新表替换旧表
                sizeCtl = (n << 1) - (n >>> 1); // 设置新阈值:容量 * 0.75(n << 1 是新容量)
                return;                    // 扩容完成,返回
            }
            // 否则当前线程完成了自己的任务,尝试将 sizeCtl 减 1(表示自己退出扩容)
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                // 如果 (sc - 2) 不等于扩容戳左移的值,说明自己不是最后一个退出的线程,直接返回
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                // 如果自己是最后一个退出的线程,设置 finishing = advance = true,并令 i = n(重新检查一遍)
                finishing = advance = true;
                i = n; // 重新扫描旧表,确保所有桶都已标记为 ForwardingNode
            }
        }

        // 6. 处理当前桶 i(如果还未被迁移)
        else if ((f = tabAt(tab, i)) == null)
            // 如果桶为空,直接 CAS 放入 ForwardingNode 节点,标记为已迁移
            advance = casTabAt(tab, i, null, fwd);

        // 如果桶首节点 hash == MOVED,说明已经被其他线程迁移过了,跳过
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed

        // 7. 桶非空且未被迁移,开始实际迁移(加锁,只锁当前桶首节点)
        else {
            synchronized (f) {
                // 双重检查,确保首节点未被其他线程修改
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn; // 低位链表(留在原位)和高位链表(搬到 i+n)

                    // -------------------- 处理链表 --------------------
                    if (fh >= 0) {
                        // 1) 先遍历一次,找到最后一个相邻的、哈希高低位相同的连续节点 lastRun
                        int runBit = fh & n;  // 根据旧表长度 n 计算高位标志(0 或 n)
                        Node<K,V> lastRun = f;
                        for (Node<K,V> p = f.next; p != null; p = p.next) {
                            int b = p.hash & n;
                            if (b != runBit) {
                                runBit = b;
                                lastRun = p;
                            }
                        }
                        // 2) 根据 runBit 分类 lastRun 和后续节点
                        if (runBit == 0) {
                            ln = lastRun;
                            hn = null;
                        } else {
                            hn = lastRun;
                            ln = null;
                        }
                        // 3) 从首节点到 lastRun 之前的节点,重新创建节点并按高低位放入两个链表
                        for (Node<K,V> p = f; p != lastRun; p = p.next) {
                            int ph = p.hash; K pk = p.key; V pv = p.val;
                            if ((ph & n) == 0)
                                ln = new Node<K,V>(ph, pk, pv, ln);
                            else
                                hn = new Node<K,V>(ph, pk, pv, hn);
                        }
                        // 4) 将 ln 放到新表原索引 i,hn 放到新表 i+n
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 5) 旧表对应位置标记为 ForwardingNode
                        setTabAt(tab, i, fwd);
                        advance = true; // 本桶迁移完成,可以推进下一个
                    }

                    // -------------------- 处理红黑树 --------------------
                    else if (f instanceof TreeBin) {
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> lo = null, loTail = null; // 低位树
                        TreeNode<K,V> hi = null, hiTail = null; // 高位树
                        int lc = 0, hc = 0;
                        // 遍历树的所有节点,按 (hash & n) 分为两组
                        for (Node<K,V> e = t.first; e != null; e = e.next) {
                            int h = e.hash;
                            TreeNode<K,V> p = new TreeNode<K,V>
                                (h, e.key, e.val, null, null);
                            if ((h & n) == 0) {
                                if ((p.prev = loTail) == null)
                                    lo = p;
                                else
                                    loTail.next = p;
                                loTail = p;
                                ++lc;
                            } else {
                                if ((p.prev = hiTail) == null)
                                    hi = p;
                                else
                                    hiTail.next = p;
                                hiTail = p;
                                ++hc;
                            }
                        }
                        // 如果拆分后的树节点数 <= UNTREEIFY_THRESHOLD(6),则退化为链表
                        ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                             (hc != 0) ? new TreeBin<K,V>(lo) : t;
                        hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                             (lc != 0) ? new TreeBin<K,V>(hi) : t;
                        // 放入新表
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        // 旧表标记为已迁移
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                }
            }
        }
    }
}

4.2、get

  • 首节点命中:比较hash和key,直接返回value

  • 特殊节点:(hash<0)

    • -1 (ForwardingNode):find 去新表 nextTable 找。

    • -2 (TreeBin):find 进红黑树找。

  • 普通链表:遍历next逐个比较,找到返回,找不到null。

    /**

    • 根据 key 获取对应的 value。

    • 核心特点:无锁(Lock-Free)读取,依靠 volatile 保证可见性。

    • 即使有线程正在扩容,get 也能正确找到数据(通过 ForwardingNode 去新表找)。
      */
      public V get(Object key) {
      // 1. 声明局部变量:tab 引用当前哈希表,e 为桶首节点,p 为查找结果,n 为数组长度
      Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;

      // 2. 扰动计算:对 key 的 hashCode 进行高低位混合,减少碰撞
      int h = spread(key.hashCode());

      // 3. 定位桶位,并检查表和桶是否非空
      // 关键点:将 table 赋值给局部变量 tab,即使后续 table 被替换(扩容完成),
      // 本次 get 依然在旧数组上操作(但旧数组的 ForwardingNode 会引导去新表)
      if ((tab = table) != null && (n = tab.length) > 0 &&
      (e = tabAt(tab, (n - 1) & h)) != null) {

      复制代码
       // 4. 情况一:桶的首节点正好就是要找的节点(hash 和 key 都匹配)
       if ((eh = e.hash) == h) {
           // 4.1 校验 key 的引用相等或 equals 相等
           if ((ek = e.key) == key || (ek != null && key.equals(ek)))
               return e.val; // 直接返回 value
       }
       
       // 5. 情况二:首节点 hash 值小于 0(特殊节点)
       //    - 如果是 ForwardingNode(hash = -1):说明该桶正在扩容,节点已被迁移到新表
       //    - 如果是 TreeBin(hash = -2):说明该桶已转为红黑树
       else if (eh < 0)
           // 调用节点自身的 find 方法进行查找(多态特性)
           // ForwardingNode.find -> 去 nextTable 里找
           // TreeBin.find -> 在红黑树里遍历查找
           return (p = e.find(h, key)) != null ? p.val : null;
       
       // 6. 情况三:普通链表结构(eh >= 0),遍历链表进行查找
       while ((e = e.next) != null) {
           if (e.hash == h &&
               ((ek = e.key) == key || (ek != null && key.equals(ek))))
               return e.val; // 找到则返回
       }

      }
      // 7. 未找到任何匹配,返回 null
      return null;
      }

4.2.1、ForwardingNode

复制代码
/**
 * ForwardingNode 是 Node 的子类,hash 固定为 -1(MOVED)
 */
static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable; // 指向新表

    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }

    // 重写了 Node 的 find 方法
    Node<K,V> find(int h, Object k) {
        // 直接拿着新表 nextTable 去查找
        outer: for (Node<K,V>[] tab = nextTable;;) {
            Node<K,V> e; int n;
            if (k == null || tab == null || (n = tab.length) == 0 ||
                (e = tabAt(tab, (n - 1) & h)) == null)
                return null;
            for (;;) {
                int eh; K ek;
                if ((eh = e.hash) == h &&
                    ((ek = e.key) == k || (ek != null && k.equals(ek))))
                    return e;
                if (eh < 0) {
                    if (e instanceof ForwardingNode) {
                        // 极端情况:新表又发生了扩容,继续往新表跳
                        tab = ((ForwardingNode<K,V>)e).nextTable;
                        continue outer;
                    }
                    else
                        return e.find(h, k); // 树节点递归
                }
                if ((e = e.next) == null)
                    return null;
            }
        }
    }
}

4.3、addcount

为了不让所有线程争抢同一个计数器,ConcurrentHashMap采用了分段计数思想,使用了两个关键字段:

  • baseCount基础计数器:voliatile long类型,记录元素个数的基础值
  • CounterCell\[\](计数单元数组):当 baseCount 发生争用时,线程会转而修改自己专属的 CounterCell 单元,把竞争压力分散到多个变量上。

源码定义:

复制代码
private transient volatile long baseCount;
private transient volatile CounterCell[] counterCells;

执行流程:

  • 优先尝试 CAS 修改 baseCount:U.compareAndSwapLong(this, BASECOUNT, v, v + x)
  • 如果 CAS 失败(说明有竞争):线程会通过 CounterCell 数组进行线程本地计数,每个线程最终只会修改自己映射到的那个 CounterCell 对象
  • 扩容联动:addCount 在修改完计数后,还会检查是否达到阈值并触发扩容(您之前看到的 put 线程参与扩容就是从这里进来的)。
java 复制代码
/**
 * 更新元素计数,并视情况触发扩容。
 * 
 * @param x     要增加的数量(通常为 1)
 * @param check 若 >= 0,则检查扩容条件;若 <= 1 则可能跳过扩容检查(性能优化)
 */
private final void addCount(long x, int check) {
    // ---------- 第一部分:更新计数(baseCount / CounterCell) ----------
    CounterCell[] as; long b, s;
    
    // 条件判断:如果 counterCells 已经存在,或者 CAS 更新 baseCount 失败,则进入分段计数逻辑
    // 注意:这里利用了短路运算,先判断 as != null,如果为 true 则直接进入 if 体;
    // 如果 as == null,则尝试 CAS 修改 baseCount,若失败也进入 if 体。
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        
        CounterCell a; long v; int m;
        boolean uncontended = true; // 表示是否无竞争
        
        // 尝试将计数累加到当前线程对应的 CounterCell 槽位
        // 条件判断(层层递进,失败则调用 fullAddCount 兜底):
        // 1. as == null:counterCells 尚未初始化
        // 2. (m = as.length - 1) < 0:数组长度为 0(理论上不可能,但防御)
        // 3. (a = as[ThreadLocalRandom.getProbe() & m]) == null:当前线程的槽位为空
        // 4. !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x)):
        //    槽位不为空,但 CAS 更新该槽位的值失败(说明有竞争)
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 兜底方法:自旋 + 重新初始化/扩容 counterCells,确保计数成功
            fullAddCount(x, uncontended);
            return; // 计数完成后直接返回,不再执行后续扩容检查(因为 fullAddCount 内部不检查扩容)
        }
        
        // 如果 check <= 1,说明本次 put 没有发生哈希冲突(链表长度为 0 或 1),
        // 此时大概率元素总数远未达阈值,跳过扩容检查以提升性能。
        if (check <= 1)
            return;
        
        // 如果成功更新了 CounterCell,但 check > 1,则需要重新计算总元素数(sumCount),
        // 用于后续扩容检查。
        s = sumCount();
    }
    // 如果 CAS 更新 baseCount 成功,则直接使用 s = b + x(已更新后的总数),
    // 并继续执行下面的扩容检查。
    
    // ---------- 第二部分:检查是否需要扩容 ----------
    // 仅当 check >= 0 时才检查(putVal 中插入新节点时传 binCount,覆盖旧值时不传负值)
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        // 循环判断条件:
        // 1. s(当前元素总数) >= sizeCtl(扩容阈值)
        // 2. table 不为空
        // 3. 当前数组长度 n < MAXIMUM_CAPACITY(最大容量)
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            
            // 生成扩容戳(基于 n 的特定编码,16 位)
            int rs = resizeStamp(n);
            
            // 情况一:已有其他线程在扩容(sc < 0)
            if (sc < 0) {
                // 判断当前线程是否应该加入协助扩容,以下任一条件满足则退出循环不参与:
                // 1. 扩容批次戳不匹配((sc >>> RESIZE_STAMP_SHIFT) != rs)
                //    说明当前扩容批次与本次要触发的扩容不一致(可能旧扩容已完成)
                // 2. sc == rs + 1:表示只剩下一个线程(第一个发起者),扩容即将结束,无需再加
                // 3. sc == rs + MAX_RESIZERS:参与线程数已达上限
                // 4. (nt = nextTable) == null:新表还未初始化(罕见)
                // 5. transferIndex <= 0:所有迁移区间都已被认领完毕
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                
                // 通过 CAS 将 sizeCtl 加 1(低 16 位 +1),表示新增一个线程参与扩容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt); // 参与扩容(协助迁移)
            }
            // 情况二:没有线程在扩容(sc >= 0),当前线程作为第一个发起者
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2)) {
                // 设置 sizeCtl = (rs << 16) + 2,高 16 位为扩容戳,低 16 位为 2(表示有 1 个线程 + 保留位)
                transfer(tab, null); // 发起扩容,nextTab 为 null 表示需要新建新表
            }
            // 更新 s 为最新的元素总数(因为其他线程可能并发修改了计数),继续循环判断
            s = sumCount();
        }
    }
}

4.4、size

复制代码
    /**
     * {@inheritDoc}
     */
    public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

baseCount和CounterCell累加则可以得到容器的长度

复制代码
 final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

5、亮点

5.1、锁粒度在桶首节点(减小锁粒度)

  • 调用put放入桶节点不为空的数据,只会在当前桶首节点加synchronized
  • 扩容过程中,迁移也是按当前桶首节点增加sunchronized

5.2、高低位扩容

  • sizeCtl,高16位resizeStamp扩容戳,低16位是参与扩容线程的数量
  • put入口遇到MOVED被动的协助扩容
  • addCount中容量操作阈值的主动触发扩容
  • 理论上就是一个扩容线程负责一段区间,实现上其实就是一个变量等于旧数组的长度,随机计算每个线程可以处理的区间长度最小是16,来一个线程长度较少16,所以他是从索引位置大位置开始迁移

5.3、扩容对读取完全透明

  • 正在扩容时,get不需要等待,依旧能瞬间返回正确的值
  • 利用多态的find方法。ForwardingNode转发节点重写了find,将查询自动跳转到新表nexttable

5.4、链表迁移批量性能好

  • 一个旧链表拆解为低位链表和高位链表,然后原子性地批量插入到新表的位置
  • 旧表长度是 n(2 的幂),扩容后新表长度是 2n
  • 旧链表上的数据对应新索引的位置只能为i或者i+n
  • 生成两个新链表其实也会是为了保证不修改原链表,保证get功能正常

5.5、addCount分段计数

  • 解决了所有线程都去抢一个size变量,CAS会剧烈冲突,性能奔溃
  • 让大多数线程只修改自己专属的CounterCell槽位,最后size在汇总
相关推荐
莫得感情 o1 小时前
设计模式 22 · 三个冷门模式:中介者、访问者、解释器
java·设计模式
andongni2031 小时前
Spring Boot基础应用开发与部署
java·spring boot·后端
lupai1 小时前
增值税发票 OCR 识别 API 新手接入指南
java·前端·ocr
TELL5211 小时前
Sonar质量门禁
java
java修仙传1 小时前
从网页禅道到 AI 能调用的工具:我的禅道 MCP 实现思路分享
java·人工智能·python·ai应用·mcp开发
HAYDENR1 小时前
依赖数据迁移工具做增量同步有哪些易错点?调整数据迁移工具策略怎么保证断点续传可靠?
java·大数据·数据库
宿6741 小时前
vue3-指令和事件处理
开发语言·前端·javascript
IT利刃出鞘2 小时前
SpringBoot--解决@Valid放在接口的List上时无效的问题
java·spring
mqiqe2 小时前
AgentScope Java Harness:2. 上下文压缩:让长期 Agent 永不“失忆“
java·开发语言