【java基础内容】ConcurrentHashmap源码万字解析

前言:

在此之前我们学习了hashmap,通过源码解析,一步步介绍,在当时我们知道他是线程不安全的,所以在高并发环境下,它没办法使用,这时候就可以使用线程安全的ConcurrentHashmap。

如果还不知道hashmap可以参考我之前写的。https://blog.csdn.net/uybji/article/details/147932178?spm=1001.2014.3001.5501

ConcurrentHashmap简介

数据结构

在 JDK 1.8 中,ConcurrentHashMap 的数据结构与 HashMap 非常相似:数组 + 链表 + 红黑树 。数组的每个元素是一个 Node 节点,当链表长度超过阈值(8)时,会转换为红黑树(TreeNode)。但 ConcurrentHashMap 引入了一些辅助结构来控制并发,例如:

  • Node 类:基础的链表节点。

  • TreeNode:红黑树节点。

  • TreeBin:用于包装红黑树的根节点,充当锁的角色。

  • ForwardingNode:扩容时出现在旧数组中的特殊节点,表示该桶已迁移。

重要的成员变量与常量

初始化

ConcurrentHashMap 采用懒加载方式,构造时只会设置一些参数(如初始容量、负载因子),真正的数组创建发生在第一次插入操作时。和hashmap是一样的。

这里的 sizeCtl 被设置为大于 0 的数,表示下一次扩容的阈值(类似 HashMapthreshold)。注意 tableSizeFor 方法用于计算大于等于给定值的最小 2 的幂。

put 操作

这是它的核心操作,我们根据源码来一点点分析

同样调用put之后会调用putval,接下来看看他的源码

java 复制代码
          
```java
/**
 * ConcurrentHashMap的核心put方法实现,支持普通put和putIfAbsent操作
 * 实现了线程安全的键值对存储,支持高并发环境下的高效操作
 *
 * @param key 要插入的键,不能为null
 * @param value 要插入的值,不能为null
 * @param onlyIfAbsent 如果为true,则只有当键不存在时才插入/更新值
 *                     如果为false,则无论键是否存在都会插入/更新值
 * @return 如果键已存在,返回旧值;如果键不存在或onlyIfAbsent为true且键存在,返回null
 */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    // ConcurrentHashMap不允许null键和null值
    if (key == null || value == null) throw new NullPointerException();
    
    // 计算键的哈希值,使用spread方法增强哈希分布,减少冲突
    int hash = spread(key.hashCode());
    
    // 用于记录当前桶中节点数量,用于后续可能的链表转红黑树判断
    int binCount = 0;
    
    // 无限循环,确保操作最终成功(CAS失败等情况会重试)
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        
        // 情况1:哈希表尚未初始化或为空
        if (tab == null || (n = tab.length) == 0)
            // 初始化哈希表
            tab = initTable();
        
        // 情况2:计算键在表中的位置,且该位置为空桶
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 使用CAS操作无锁插入新节点
            // 成功则跳出循环,失败则重试(可能有其他线程已插入)
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   // 空桶插入不需要加锁
        }
        
        // 情况3:当前桶的头节点是MOVED状态,表示正在扩容
        else if ((fh = f.hash) == MOVED)
            // 帮助完成扩容,返回扩容后的新表
            tab = helpTransfer(tab, f);
        
        // 情况4:检查是否满足putIfAbsent的快速返回条件
        // 仅在onlyIfAbsent为true时检查,无需加锁,提高效率
        else if (onlyIfAbsent // 仅当onlyIfAbsent为true时才进入此分支
                 && fh == hash // 哈希值匹配
                 && ((fk = f.key) == key || (fk != null && key.equals(fk))) // 键匹配
                 && (fv = f.val) != null) // 值不为null
            // 键已存在且值不为null,直接返回旧值,不进行更新
            return fv;
        
        // 情况5:需要对桶进行加锁操作(链表或红黑树操作)
        else {
            V oldVal = null;
            // 对当前桶的头节点加锁,确保线程安全
            synchronized (f) {
                // 双重检查:确保头节点未被其他线程修改
                if (tabAt(tab, i) == f) {
                    // 子情况5.1:头节点的哈希值>=0,表示是普通链表结构
                    if (fh >= 0) {
                        binCount = 1; // 链表节点计数从1开始
                        // 遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 找到匹配的键
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val; // 记录旧值
                                // 如果不是onlyIfAbsent模式,更新值
                                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);
                                break; // 插入完成,跳出循环
                            }
                        }
                    }
                    // 子情况5.2:头节点是TreeBin,表示是红黑树结构
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2; // 红黑树的binCount从2开始
                        // 在红黑树中插入或更新节点
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val; // 记录旧值
                            // 如果不是onlyIfAbsent模式,更新值
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    // 子情况5.3:头节点是ReservationNode(保留节点)
                    // 这种情况不应该发生在常规put操作中
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            
            // 操作完成后检查
            if (binCount != 0) {
                // 如果链表长度达到树化阈值,将链表转换为红黑树
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                // 如果找到了旧值,返回旧值
                if (oldVal != null)
                    return oldVal;
                // 插入了新节点,跳出循环
                break;
            }
        }
    }
    
    // 更新元素计数,并可能触发扩容
    addCount(1L, binCount);
    
    // 插入了新节点,返回null
    return null;
}
```
        

注意:在这个时候我们对比hashmap和它的计算hash值的过程可以发现(上面是hashmap)

它多了一个& HASH_BITS这个操作,这是因为,在concurrenhashmap中hash值会有负数,来表示一些情况所以要进行这个操作防止hash值为负数产生混淆:

  • MOVED = -1(表示节点正在迁移)

  • TREEBIN = -2(表示树根节点)

  • RESERVED = -3(表示占位节点)

我们一点点来分析

进行非空判断,这是因为concurrenhashmap不支持null值(这是因为在高并发环境下,get获取值为null的话无法分辨到底是key不存在所以返回null,还是因为值就是null,防止产生两异性,并且我们可以看见上面计算hash值时,hashmap是判空了的而它没有

然后进行hash值计算,binCount表示数组桶的个数。

第一个if我们进行数组初始化,在刚刚说了初始化concurrehashmap的时候我们是没有创建数组的,当第一次调用put时才会进行数组初始化。

第二个else if 表示头节点为空的话直接用CAS进行插入如果成功直接break跳出循环。

复制代码
i = (n - 1) \& hash)表示数组下标
复制代码
tabAt(tab, i = (n - 1) \& hash))根据下标获取数组头节点,tabAt是java Usafe包中提供的方法,后面会介绍。

第三个else if 表示算出来的hash值为-1,这个时候表示现在数组在进行扩容,当前线程会直接调用进行协助扩容,因为现在是多线程。

第四个else if表示在 ConcurrentHashMapputVal 方法中,onlyIfAbsent 参数的含义是:当且仅当键不存在时才插入 。如果键已存在,则不进行任何更新,直接返回已有的值

可以看见现在加锁了,所以才说它是线程安全的,不过这个锁是一个桶级锁,只会对数组其中一个点生效,其他点是不影响的。

双重判定,当拿到锁之后,还要拿一次头节点与现有的头节点比较,这是因为多线程,我们在拿到锁的一瞬间也许,其他线程就把当前头节点改变了,使得最新的数组结构可能就不再是链表而是红黑树,这时候如果当前线程继续按旧结构去操作,这会产生数据结构破坏。

现在看到的就是操作链表的过程,为什么当fh>0就表示链表,因为fh这时候是hash值,在最开始就对hash值做了处理不可能为负数,而树结构的fh是-2,所以只能是链表。

剩下的两个情况分别是树结构的处理,和表示头节点是保留节点,但是在put中不可能有这种情况所以直接抛异常

进行判断,如果bincount>8表示要转换为树结构,因为bincount表示的是链表个数。

大家注意虽然已经调用了,但是它里面会判断,要是数组容量<64的时候还是会优先扩容而不是转变为树。

它有两个作用一个是更新数组元素个数,一个是进行扩容判断。

sizeCtl的含义

sizeCtlConcurrentHashMap 中一个至关重要的控制变量,用于协调多线程的初始化和扩容操作。它的值在不同阶段代表不同含义,设计精巧,是理解并发扩容机制的关键。

负数状态:表示正在进行初始化或扩容

  • -1 :表示当前有线程正在初始化哈希表(table 数组)------即 initTable() 方法正在执行,其他线程遇到此值会自旋等待。

  • 其他负数(< -1):表示哈希表正在扩容,具体数值由两部分组成:

    • 高 16 位:扩容标识戳,基于当前容量生成,用于标识本次扩容的唯一性。

    • 低 16 位:参与扩容的线程数加 1

    例如,若 sizeCtl = (resizeStamp << 16) + 2,表示当前容量为 n 的扩容正在进行中,已有一个线程在迁移数据;当该值变为 (resizeStamp << 16) + 3 时,说明有第二个线程加入。


2. 正数状态:表示阈值或初始容量

  • table 未初始化sizeCtl 存储的是构造时传入的初始容量(如果传入的是 0,则暂为 0,后续在第一次 put 时会被设为默认值 16 对应的阈值)。

  • table 已初始化sizeCtl 存储的是下一次触发扩容的阈值,计算公式为 容量 * 负载因子(负载因子固定为 0.75)。当元素个数超过该值时,将触发扩容。


3. 零状态

  • 表示构造时未指定初始容量,且 table 尚未初始化。在第一次插入时,sizeCtl 会被设置为默认容量(16)对应的阈值(12)。

addCount源码

java 复制代码
          
```java
/**
 * Adds to count, and if table is too small and not already
 * resizing, initiates transfer. If already resizing, helps
 * perform transfer if work is available.  Rechecks occupancy
 * after a transfer to see if another resize is already needed
 * because resizings are lagging additions.
 *
 * @param x the count to add (通常为1,表示新增一个元素)
 * @param check if <0, don't check resize (不检查扩容), if <= 1 only check if uncontended (仅在无竞争时检查扩容)
 */
private final void addCount(long x, int check) {
    CounterCell[] cs; long b, s;
    
    // 阶段1: 并发计数逻辑 - 采用两阶段计数策略提高并发性能
    // 策略1: 优先使用baseCount进行计数 (无竞争时效率最高)
    // 如果counterCells不为空,或者CAS更新baseCount失败(发生竞争),则进入策略2
    if ((cs = counterCells) != null ||
        !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell c; long v; int m;
        boolean uncontended = true; // 标记是否无竞争
        
        // 策略2: 使用CounterCell数组分散计数 (有竞争时使用)
        // 检查条件:
        // 1. counterCells数组为空
        // 2. 数组长度小于等于0 (无效)
        // 3. 当前线程映射到的CounterCell为空 (首次访问)
        // 4. CAS更新CounterCell的值失败 (该Cell发生竞争)
        if (cs == null || (m = cs.length - 1) < 0 ||
            (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended = // 尝试CAS更新CounterCell的值,并记录是否成功
              U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
            // 策略3: 极端竞争情况,调用fullAddCount处理 (创建/扩容CounterCell数组)
            fullAddCount(x, uncontended);
            return;
        }
        
        // 如果check <= 1且当前是无竞争情况,不进行扩容检查
        // 这种情况通常发生在对同一个桶的连续操作,避免不必要的扩容检查
        if (check <= 1)
            return;
            
        // 计算当前元素总数:baseCount + 所有CounterCell的值之和
        s = sumCount();
    }
    
    // 阶段2: 扩容检查与触发逻辑
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        
        // 循环检查扩容条件:
        // 1. 当前元素总数s >= 扩容阈值sc
        // 2. 哈希表不为空
        // 3. 当前容量小于最大容量(2^30)
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            // 生成扩容标记:将当前容量n传入resizeStamp生成唯一标记,然后左移RESIZE_STAMP_SHIFT位
            // 该标记用于后续线程间的扩容状态协调
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
            
            // 情况1: 已有线程在扩容 (sc < 0表示扩容状态)
            if (sc < 0) {
                // 扩容退出条件检查:
                // 1. sc == rs + MAX_RESIZERS: 达到最大协助线程数
                // 2. sc == rs + 1: 扩容即将完成 (所有桶已分配)
                // 3. nextTable == null: 扩容已结束
                // 4. transferIndex <= 0: 没有更多桶需要迁移
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    (nt = nextTable) == null || transferIndex <= 0)
                    break; // 无法协助扩容,退出循环
                    
                // 尝试CAS将sc加1,表示当前线程加入扩容队伍
                // 成功则调用transfer方法协助扩容
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt); // 协助已有扩容操作
            }
            // 情况2: 无其他线程在扩容,尝试触发新的扩容
            else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null); // 首次触发扩容,nextTable为null
                
            // 扩容后重新计算元素总数,防止扩容过程中元素增长过快需要再次扩容
            s = sumCount();
        }
    }
}
```
        

第一个功能,数组元素增加

扩容功能判定

helpTransfer源码

java 复制代码
/**
 * 协助ConcurrentHashMap进行并发扩容的核心方法
 * 
 * 当线程在操作哈希表时遇到ForwardingNode节点(表示正在进行扩容),
 * 会调用此方法参与到扩容过程中,提高扩容效率
 * 
 * @param tab 当前的哈希表数组
 * @param f 当前访问到的节点,通常是一个ForwardingNode
 * @return 扩容后的新哈希表数组,如果扩容未完成则返回当前表
 */
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    
    // 检查扩容条件是否满足:
    // 1. tab不为空
    // 2. 当前节点f是ForwardingNode实例(表示正在扩容)
    // 3. 能够从ForwardingNode中获取到有效的新表nextTab
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        
        // 生成扩容标记:使用当前表长度计算resizeStamp,然后左移RESIZE_STAMP_SHIFT位
        // 此标记用于后续线程间的扩容状态协调和验证
        int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
        
        // 循环条件:
        // 1. nextTab == nextTable:确保扩容仍在进行(新表未被替换)
        // 2. table == tab:确保当前表未被其他线程替换
        // 3. sizeCtl < 0:确保仍处于扩容状态
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            
            // 退出循环条件检查:
            // 1. sc == rs + MAX_RESIZERS:已达到最大协助线程数
            // 2. sc == rs + 1:扩容即将完成(所有桶已分配完毕)
            // 3. transferIndex <= 0:没有更多桶需要转移
            if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                transferIndex <= 0)
                break;
            
            // 使用CAS操作尝试将sizeCtl加1,表示当前线程加入扩容团队
            // 成功则调用transfer方法执行实际的数据迁移工作
            if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);  // 执行数据迁移
                break;  // 完成协助后退出循环
            }
        }
        
        // 返回扩容后的新表
        return nextTab;
    }
    
    // 未满足扩容条件或扩容已完成,返回当前表
    return table;
}
        

transfer源码非常重要

因为我们在面试的时候一般也是问的扩容机制,扩容其实也就是创建一个新数组,然后把旧数组的元素迁移到新数组,它就是扩容的主要实现,接下来我们一点点来看

java 复制代码
          

/**
     * ConcurrentHashMap的核心扩容方法,负责将旧表中的节点迁移/复制到新表
     * 支持多线程并发扩容,提高扩容效率
     * 
     * @param tab 旧的哈希表数组
     * @param nextTab 新的哈希表数组(容量是旧表的2倍),首次调用时为null
     */
    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        
        // 计算每个线程处理的桶数量:
        // - 多核CPU下:(n >>> 3)/NCPU 即 n/8/NCPU,确保每个CPU核处理的桶数大致均匀
        // - 单核CPU下:n(整个表由一个线程处理)
        // - 确保最小处理粒度不小于MIN_TRANSFER_STRIDE(默认为16),避免过多线程竞争
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // 保证最小处理粒度
        
        // 初始化阶段:如果nextTab为null,表示当前线程是第一个发起扩容的线程
        if (nextTab == null) {            // initiating
            try {
                // 创建新表,容量是旧表的2倍(n << 1)
                @SuppressWarnings("unchecked")
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
                nextTab = nt;
            } catch (Throwable ex) {      // 处理OOM异常(内存不足)
                sizeCtl = Integer.MAX_VALUE; // 设置一个极大值,标记扩容失败
                return;
            }
            nextTable = nextTab; // 设置全局变量,通知其他线程正在进行扩容
            transferIndex = n;   // 设置迁移索引,从旧表末尾开始分配任务
        }
        
        int nextn = nextTab.length;
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab); // 创建转发节点
        boolean advance = true;  // 是否需要推进到下一个桶
        boolean finishing = false; // 是否完成扩容的最后阶段
        
        // 主循环:处理桶的迁移任务
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            
            // 任务分配阶段:获取当前线程需要处理的桶范围
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false; // 已经处理完当前分配的桶或正在完成阶段
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;  // 没有更多桶需要处理
                    advance = false;
                }
                // 使用CAS原子更新transferIndex,分配stride个桶给当前线程
                else if (U.compareAndSetInt
                         (this, TRANSFERINDEX, nextIndex,
                          nextBound = (nextIndex > stride ?
                                       nextIndex - stride : 0))) {
                    bound = nextBound;  // 当前线程处理的桶的下界
                    i = nextIndex - 1;  // 当前线程处理的第一个桶(从后往前)
                    advance = false;
                }
            }
            
            // 边界检查和完成处理
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) { // 完成扩容的最后阶段
                    nextTable = null;  // 清空临时变量
                    table = nextTab;   // 更新全局表引用为新表
                    // 设置新的sizeCtl:(2n) - (n/2) = 1.5n,作为下一次扩容的阈值
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }
                // 减少正在扩容的线程计数
                if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    // 检查是否所有线程都已完成扩容
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    // 进入完成阶段
                    finishing = advance = true;
                    i = n; // 重新检查所有桶
                }
            }
            // 桶为空:直接放置转发节点
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // 桶已经被处理过:跳过
            else if ((fh = f.hash) == MOVED)
                advance = true; // 已被其他线程处理
            // 处理非空桶
            else {
                synchronized (f) { // 对桶的头节点加锁,保证线程安全
                    if (tabAt(tab, i) == f) { // 双重检查头节点是否未被修改
                        Node<K,V> ln, hn; // 低位链表和高位链表
                        
                        // 处理普通链表节点(fh >= 0)
                        if (fh >= 0) {
                            int runBit = fh & n; // 计算节点在新表中的位置(0或n)
                            Node<K,V> lastRun = f; // 记录最后一个连续相同runBit的节点
                            
                            // 查找lastRun:从链表中找到最后一个连续相同runBit的节点
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            
                            // 根据lastRun的runBit确定低位和高位链表
                            if (runBit == 0) {
                                ln = lastRun; // ln指向lastRun,hn为null
                                hn = null;
                            }
                            else {
                                hn = lastRun; // hn指向lastRun,ln为null
                                ln = null;
                            }
                            
                            // 将链表节点分配到低位或高位链表
                            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); // 头插法构建高位链表
                            }
                            
                            // 将低位链表放入新表的i位置
                            setTabAt(nextTab, i, ln);
                            // 将高位链表放入新表的i+n位置
                            setTabAt(nextTab, i + n, hn);
                            // 在旧表的i位置放置转发节点,标记该桶已处理
                            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; // 低位和高位节点计数
                            
                            // 将树节点分配到低位或高位链表
                            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);
                            // 在旧表的i位置放置转发节点,标记该桶已处理
                            setTabAt(tab, i, fwd);
                            advance = true; // 继续处理下一个桶
                        }
                        // 处理预留节点(不应该出现在这里)
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
            }
        }
    }

        

看着很大一串,我们来一部分一部分的看

这一部分其实就是计算每个线程的工作量,因为我们知道线程会进行协助扩容,那每个线程应该如何分配,就是靠这个,但是每个线程最少也要分到16个桶。

这段其实就是当第一个线程进入扩容时,他会创建一个新数组,也就是第一次扩容时。

这段其实就是更细致的任务分配,刚刚算的每个线程最少不低于16个桶,这里就是计算每个线程从哪个节点到哪个节点,工作范围

这一段,两个if,第一个if表示获取到的头节点为空时,直接给adcance赋值fwd,这个时候表示此点已经被转移完毕,可以忽略了,那么此时线程就会前往下一个节点。就相当于一个记号。

第二个if表示此节点hash为-1,我们说过-1表示正在扩容,所以此时这个点已经有线程在工作,那么其他线程就可以前往其他点。

这个就是结尾工作,对每个桶进行检查,还会把sizeCtl设置为下一次的扩容阈值。

java 复制代码
else {
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        if (fh >= 0) {
                            int runBit = fh & 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;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            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);
                            }
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            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;
                            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;
                                }
                            }
                            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;
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }

这段代码其实就是对链表结构或者树结构进行数据迁移的,把他们从旧数组给弄到新数组。在hashmap中说过,迁移到坐标是根据每个点的hash值对旧数组容量进行按位与操作,当结果为零,那么新位置就是原位置,否则新位置就是hash值+旧容量,不理解可以去我之前写的看看。

tabAt casTabAt setTabAt

tabAt:保证你拿到的就是最新的

casTabAt:保证数据不会被直接覆盖,比如在更新时,当一个线程要更新,会查看此时的点还是不是之前旧的那个,如果不是就说明已经被其他线程修改,那么它就会重新进行,put的流程,就不会把别人刚刚修改的数据给覆盖,保证了线程安全。它就是CAS的具体实现。

setTabAt:确定要覆盖时用,保证"写完别人立刻看见"

他们三个就是线程安全的实现者,在concurrenthashmap的线程安全中,就是靠他们实现了无锁线程安全,所以呀concurrenthashmap加锁的场景很少,性能也比较好。

ConcurrentHashMap扩容机制

两种情况第一是当链表大于8但是数组小于64,第二种就是元素个数超过了阈值

ConcurrentHashMap 的扩容机制是多线程协同迁移,核心流程如下:

  1. 触发时机 :每次 put 操作后,元素个数超过 sizeCtl(容量×0.75)时触发扩容。

  2. 新数组创建:第一个触发扩容的线程创建新数组,大小为旧数组的两倍。

  3. 任务分片 :有一个全局的 transferIndex 指针,从旧数组末尾开始向前分配任务。每个线程通过 CAS 竞争领取一段连续的桶(默认最小 16 个),领取后负责迁移这段区间内的所有桶。

  4. 桶迁移

    • 对每个桶的头节点加锁(synchronized),防止并发修改。

    • 根据桶的类型(链表或红黑树)将节点分为低位链 (留在原索引)和高位链(移到原索引+旧容量)。

    • 迁移后将高低位链放入新数组对应位置,并在旧数组的桶位置放置一个 ForwardingNode 标记,表示该桶已处理。

  5. 并发读 :读操作遇到 ForwardingNode 时,直接去新数组中查找,不影响读。

  6. 写操作协助 :如果 put 时发现桶头是 ForwardingNode,当前线程会先协助扩容,搬完桶后再继续自己的插入。

  7. 完成收尾 :所有桶迁移完后,最后一个退出扩容的线程将新数组赋值给 table,并重新计算 sizeCtl 为新阈值。

与hashmap的对比

总结:

我们通过源码解释了concurrenthashmap,大家在理解的时候对照着hashmap进行学习,最好自己也可以独自去分析一下源码看看怎么样,谢谢你的阅读,一起加油吧!

相关推荐
Scott9999HH4 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔4 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社5 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海5 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖6 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大9 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai9 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说10 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json