【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进行学习,最好自己也可以独自去分析一下源码看看怎么样,谢谢你的阅读,一起加油吧!

相关推荐
book123_0_992 小时前
Spring boot创建时常用的依赖
java·spring boot·后端
帐篷Li2 小时前
【BBF系列协议】USP/TR-369 Agent 开发计划
开发语言·python
Yupureki2 小时前
《MySQL数据库基础》4. 数据类型
c语言·开发语言·数据结构·数据库·c++·mysql
root666/2 小时前
【Java-后端-Mybatis】JOIN 作用
java·mybatis
C++ 老炮儿的技术栈2 小时前
C++、C#常用语法对比
c语言·开发语言·c++·qt·c#·visual studio
共享家95272 小时前
Java入门(继承)
java·开发语言
Bert.Cai2 小时前
Python默认参数详解
开发语言·python
loading小马2 小时前
解决jdk17版本与seata冲突问题
java·jvm·jdk·intellij-idea
_饭团2 小时前
指针核心知识:5篇系统梳理4
c语言·开发语言·c++·笔记·深度学习·算法·面试