ConcurrentHashMap
好长的名字,好难理解的map。
学习这个map,中间也遇到了很多问题,不过现在有了DeepSeek的帮助,我可以随意理解,所有不会的问题,都可以刨根问底,虽然确实不太必要,而且浪费时间。
没关系,整理一下,之后就是背八股了。
简单说一下我对于1.7之前的ConcurrentHashMap的理解,
一个map持有一个Segment数组,每个Segment元素持有一个node数组。进入的时候,先哈希运算,进入相应的Segment的元素,写操作的时候加锁,就这样。
重点说明1.8之后的ConcurrentHashMap,这里不再存在Segment元素,相反,整个map的锁的颗粒度已经达到了每一个node理解。
简单看一下,字段的解释放在后面。
构造函数
先是完整的构造函数:
            
            
              arduino
              
              
            
          
           Creates a new, empty map with an initial table size based on the given number of elements (initialCapacity), initial table density (loadFactor), and number of concurrently updating threads (concurrencyLevel).
 形参:
 initialCapacity -- the initial capacity. The implementation performs internal sizing to accommodate this many elements, given the specified load factor.
 loadFactor -- the load factor (table density) for establishing the initial table size
 concurrencyLevel -- the estimated number of concurrently updating threads. The implementation may use this value as a sizing hint.
 抛出:
 IllegalArgumentException -- if the initial capacity is negative or the load factor or concurrencyLevel are nonpositive
 
 
 public ConcurrentHashMap(int initialCapacity,
                          float loadFactor, int concurrencyLevel) {
     if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
         throw new IllegalArgumentException();
     if (initialCapacity < concurrencyLevel)   // Use at least as many bins
         initialCapacity = concurrencyLevel;   // as estimated threads
     long size = (long)(1.0 + (long)initialCapacity / loadFactor);
     int cap = (size >= (long)MAXIMUM_CAPACITY) ?
         MAXIMUM_CAPACITY : tableSizeFor((int)size);
     this.sizeCtl = cap;
 }
        这里其实就是判断参数的合法性。虽然有这么多参数,其实就是为了控制sizeCtl,它是一个很重要的参数。它是大于等于cap的最小2的n次幂。
            
            
              arduino
              
              
            
          
          tableSizeFor((int)size
/**
 * Returns a power of two table size for the given desired capacity.
 * See Hackers Delight, sec 3.2
 */
private static final int tableSizeFor(int c) {
    int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
        这个函数还有另外一种实现方式,但是我感觉没有这种实现方式优雅。实在是太优雅了。
cap * loadFactor也就得到了initialCapacity
之后进行数组操作的时候,会反复调用CAS改变它的值。如果它的值是一个正数,那么它就表示这个map的容量,如果它的值是一个负数,就表示,这个数组正在扩容。而且它的绝对值越大,扩容的线程就越多。后面会重点展示它的计算,它的计算充分展现了位运算的强大。
put方法
我们直接从put方法进入。
            
            
              typescript
              
              
            
          
          public V put(K key, V value) {
    return putVal(key, value, false);
}
        它委托给putVal来实现:
            
            
              ini
              
              
            
          
          /** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //添加值不能为null,因为null无法进行hash运算。感觉加上null,要处理的麻烦比较多。
    if (key == null || value == null) throw new NullPointerException();
    //参见后面位运算的部分,这里使用spread,把hash值充分压缩到16位
    int hash = spread(key.hashCode());
    //binCount是链表的数量,如果是红黑树,这个值固定为2,普通链表则会在后面动态+1,如果太大,就会转变成红黑树
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        //初始化table,走完这里的逻辑,就可以直接回到开头,重新循环了
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //空槽,直接设值,成功就break,失败,就重新循环。
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break;                   // no lock when adding to empty bin
        }
        //扩容,
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //逻辑可以走到这里,说明这个节点肯定存在了
        //这里先提供看一下是不是onlyIfAbsent,是的话,就存在一种可能,不需要设锁,直接拿到值就跑路。
        else if (onlyIfAbsent // check first node without acquiring lock
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
        //上锁,走你
        else {
            V oldVal = null;
            //对象锁,这里因为ConcurrentHashMap的锁的颗粒度非常细,所以这样锁也没有什么问题。
            //因为冲突的可能性不大,所以是轻量锁。
            synchronized (f) {
                //再判断一波,因为可能被锁阻塞了,那么这里f可能就已经被remove了。
                //另外,能够进入这个逻辑,说明,要么是潘通节点,要么是树节点,
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        //能进来,至少是有一个bin的,就为1,另外,因为已经加锁了,所以后面的操作都是线程安全的
                        binCount = 1;
                        //++binCount,也就是对于bin计数
                        for (Node<K,V> e = f;; ++binCount) {
                            //找节点
                            K ek;
                            //先是hash判断,之后引用判断,之后是内容判断,和HashMap一样。
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                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;
                            }
                        }
                    }
                    //树节点的fh好像是-2,为了保证和MOVE与普通的node区分。
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        //固定binCount为2,后面就不会触发treeifyBin
                        binCount = 2;
                        //红黑树查找,等我学好数据结构再回来理,如果有这样一天的话。
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                    else if (f instanceof ReservationNode)
                        throw new IllegalStateException("Recursive update");
                }
            }
            //这里就是前面的逻辑,但凡if (fh >= 0) 这里通过了,下面的逻辑就要走一波,不然没有break,就再循环一遍。
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                //oldVal 不等于null,就说明,不需要之后的逻辑了,addCount就不用走了,直接返回值就可以。
                //因为如果存在oldVal,就说明只是一次无关紧要的修改,表的容量没有变大
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //这里会根据binCount进行判断,逻辑很复杂。
    addCount(1L, binCount);
    return null;
}
        上面基本上就是全部了。
addCount
简单进入这个函数的逻辑:
            
            
              scss
              
              
            
          
          /**
 * 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
 * @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;
    //counterCells用来分段计数,如果还没有这个东西,
    if ((cs = counterCells) != null ||
        //就看能不能直接CAS给baseCount赋值,结束这段逻辑
        //能够进入这段逻辑,要么存在cs。存在cs,就说明已经有着比较激烈的竞争了,所以就不需要CAS直接赋值baseCount了
        //要么就是赋值baseCount失败
        !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell c; long v; int m;
        //uncontended表示没有竞争
        boolean uncontended = true;
        //为什么需要在这里加上一个判断cs == null呢?为了避免后面出现空指针异常,因为很可能还没有cs,
        if (cs == null || (m = cs.length - 1) < 0 ||
            //如果c槽为空。c是一个线程相关的随机数,&m表示映射到m这段区间中。
            (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              //这里表示直接赋值失败,也就是存在竞争。
              U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        //一个简单的问题是,为什么这里check <= 1就直接return了,
        //下面check >= 0还要继续走。
        //因为,如果直接进入下面的逻辑,说明上面来得太轻松了,竞争不激烈,估计整个map也不大,那么对于扩容的判断就要多一些。
        //反过来,如果进入上面的逻辑,说明已经存在竞争了,但是既然check<=1,说明map还可以用,多一事不如少一事。
        if (check <= 1)
            return;
        //直接通过cs进行求和,走的是LongAddr的逻辑,为了下面的循环。
        s = sumCount();
    }
    //check>=0,就说明存在需要扩表的可能。这里存在check=0的的情况,但是必要是前面的if没有进入,说明表很小,无冲突。
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        //while循环,因为这里存在并发问题,有可能你走到这里的时候,其他线程添加了节点导致需要扩表了。
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            //这一步的结束后面有。
            int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
            if (sc < 0) {
                //rs是一个标记值,它非常优雅,但是不好理解。它是一个最高位为1的数,换言之,一个负数。
                //但是,这里我认为,我们应该超越正负的观念,只是认为这个数最高位标记为1,表示它和寻常的状态不一样罢了。
                //transfer结束之后,sc--,所以一开始就直接rs+2,说明rs+1就是结束transfer的时刻。
                if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
                    (nt = nextTable) == null || transferIndex <= 0)
                    break;
                if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}
        以上只是简单走完了构造一个ConcurrentHashMap和往里面put一个值的过程,但是,我们还有很多问题没有解决。
transfer方式究竟是如何运作的?
为什么会出现一个helptransfer的函数。
binCount是什么?又为什么存在?
check是什么,为什么存在多个判断条件?
uncontended是什么?
rs是什么?为什么选择这样一个数作为标记?
ConcurrentHashMap里面使用的位运算有多么巧妙?
ConcurrentHashMap.addCount() 方法详细解析
一开始询问了DeepSeek,它的回答如下。我稍微删减一下。
这是 ConcurrentHashMap 中最核心的方法之一,负责元素计数和扩容触发。让我完整解释这个方法的每个部分:
📊 方法签名和变量声明
            
            
              arduino
              
              
            
          
          private final void addCount(long x, int check) {
    CounterCell[] cs; long b, s;
        - x:要增加的计数值(通常为 1 或 -1)也就表示添加或者删减的节点的数量,在putVal中,为1,表示新增一个节点。
 - check:扩容检查阈值,在putVal中,也就是binCount。
 - cs:计数单元数组引用,这里就是一个LongAddr,以空间换时间,避免多线程冲突。
 - b, s:临时存储 baseCount 和总和
 
🔢 第一部分:计数更新机制
1.1 快速路径尝试
            
            
              ini
              
              
            
          
          if ((cs = counterCells) != null ||
    !U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        逻辑:
- 如果已经存在 CounterCell 数组,或者
 - CAS 更新 baseCount 失败(说明有竞争)
 
1.2 慢速路径 - 分段计数
            
            
              ini
              
              
            
          
          CounterCell c; long v; int m;
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
    (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
    !(uncontended = U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
    fullAddCount(x, uncontended);
    return;
}
        分段计数失败条件(任一满足即进入 fullAddCount):
cs == null:CounterCell 数组未初始化(m = cs.length - 1) < 0:数组长度为0(c = cs[ThreadLocalRandom.getProbe() & m]) == null:当前线程对应的槽位为空!U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x):CAS 更新对应槽位失败
反过来说,以上任何一个成功了,后面就不会进行判断,有了cs,就不会尝试CAS,这就是短路或的优雅。,
1.3 计数完成后的检查
            
            
              ini
              
              
            
          
          if (check <= 1)
    return;
s = sumCount();  // 计算当前总元素数
        check 参数的含义:
check <= 1:不需要检查扩容(如替换现有值)check >= 0:需要检查扩容(如新增元素)
🚀 第二部分:扩容检查与触发
2.1 扩容条件判断
            
            
              ini
              
              
            
          
          if (check >= 0) {
    Node<K,V>[] tab, nt; int n, sc;
    while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
           (n = tab.length) < MAXIMUM_CAPACITY) {
        扩容触发条件:
s >= (long)(sc = sizeCtl):元素总数 ≥ 扩容阈值(tab = table) != null:哈希表已初始化(n = tab.length) < MAXIMUM_CAPACITY:未达到最大容量
2.2 扩容状态标识计算
            
            
              ini
              
              
            
          
          int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
        resizeStamp(n):生成与当前容量相关的唯一标识- 左移 
RESIZE_STAMP_SHIFT(16位)得到基础状态值 
这里是位运算,提高效率,只需要理解resizeStamp(n)是一个唯一标识就足够了。左移16位,为的就是空余来低16位用于添加线程。
2.3 扩容中状态处理(已有扩容在进行)
            
            
              ini
              
              
            
          
          if (sc < 0) {
    if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
        (nt = nextTable) == null || transferIndex <= 0)
        break;
    if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
        transfer(tab, nt);
}
        不能协助扩容的条件:
sc == rs + MAX_RESIZERS:协助线程数已达上限sc == rs + 1:扩容即将完成(nt = nextTable) == null:新表未初始化transferIndex <= 0:所有迁移任务已完成
协助扩容 :通过 CAS 将 sizeCtl + 1,然后调用 transfer 参与迁移
2.4 发起新扩容
            
            
              kotlin
              
              
            
          
          else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
    transfer(tab, null);
        第一个发起扩容的线程:
- 通过 CAS 将 
sizeCtl设置为rs + 2 - 调用 
transfer(tab, null)开始迁移,第二个参数为 null 表示需要初始化新表 
2.5 更新计数并重新检查
            
            
              ini
              
              
            
          
          s = sumCount();
        在循环末尾重新计算总元素数,因为迁移过程中可能有其他线程在操作
🎯 关键设计思想详解
1. 分段计数(CounterCell)机制
问题:高并发下频繁更新 baseCount 会成为性能瓶颈
解决方案:
            
            
              scss
              
              
            
          
          // 类似于 LongAdder 的分段计数
+----------+    +------------+------------+------------+
| baseCount|    | CounterCell| CounterCell| CounterCell|
|  (基础值) |    |   (线程1)  |   (线程2)  |   (线程3)  |
+----------+    +------------+------------+------------+
        最终计数 = baseCount + ∑CounterCell[i].value
2. sizeCtl 状态机
sizeCtl 的不同值表示不同状态:
| 值范围 | 含义 | 
|---|---|
> 0 | 
扩容阈值(容量 × 负载因子) | 
-1 | 
表正在初始化 | 
<-1 | 
高16位:扩容标识,低16位:参与扩容线程数+1 | 
举例:
- 容量=16,阈值=12:
sizeCtl = 12 - 扩容中(容量16→32,3个线程):
sizeCtl = rs + 3 
3. 扩容戳(resizeStamp)机制
            
            
              arduino
              
              
            
          
          static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
        作用:为每次扩容生成唯一标识,防止不同容量的扩容相互干扰
🔄 完整工作流程示例
场景:插入新元素触发扩容
- 线程A 插入元素,调用 
addCount(1, binCount) - 尝试快速计数:CAS 更新 baseCount 失败(有竞争)
 - 分段计数:找到对应 CounterCell,CAS 更新成功
 - 检查扩容:计算总元素数 s=13,阈值=12
 - 发起扩容 :线程A成功 CAS 设置 
sizeCtl = rs + 2 - 开始迁移 :调用 
transfer(tab, null)开始数据迁移 - 线程B 插入时发现扩容中,协助迁移
 - 完成扩容:所有线程协作完成,更新 table 引用和 sizeCtl
 
💡 性能优化亮点
1. 无锁计数
- 通过 CAS 和分段计数避免锁竞争
 - 读多写少时性能接近 volatile 变量
 
2. 协作式扩容
- 多个线程并行迁移,加速扩容过程
 - 动态调整参与线程数
 
3. 精确状态控制
- 通过 sizeCtl 精确控制扩容状态
 - 防止过度扩容或协助线程过多
 
4. 渐进式检查
- 只在真正需要时检查扩容
 - 减少不必要的计算开销
 
这个方法是 ConcurrentHashMap 高并发设计的精华体现,通过巧妙的分段计数和协作式扩容,在保证线程安全的同时实现了极高的并发性能!
Uncontende与check解析
🔍 uncontended 的作用
基本含义
uncontended 表示 "无竞争" ,用于标记在 CounterCell 更新过程中是否遇到了竞争。
在代码中的使用
            
            
              ini
              
              
            
          
          boolean uncontended = true;  // 初始假设无竞争
if (cs == null || (m = cs.length - 1) < 0 ||
    (c = cs[ThreadLocalRandom.getProbe() & m]) == null ||
    !(uncontended = U.compareAndSetLong(c, CELLVALUE, v = c.value, v + x))) {
    // 如果进入这里,说明有竞争或初始化问题
    fullAddCount(x, uncontended);
    return;
}
        uncontended 的三种状态
- 
true:真正无竞争(理想情况)- CounterCell 数组存在且长度正常
 - 当前线程对应的槽位存在
 - CAS 更新 CounterCell 成功
 
 - 
false:有竞争- CounterCell 数组和槽位都正常
 - 但 CAS 更新 CounterCell 失败(其他线程在同时更新)
 
 - 
初始化问题:
- CounterCell 数组为 null 或长度为0
 - 当前线程对应的槽位为 null
 
 
uncontended 在 fullAddCount 中的作用
        
            
            
              scss
              
              
            
          
          fullAddCount(x, uncontended);
        uncontended 参数告诉 fullAddCount:
true:可能是初始化问题,需要初始化 CounterCell 数组或槽位false:明确有竞争,需要重试或扩容 CounterCell 数组
🎯 check 参数的巧妙设计
check 参数的含义
check 参数控制扩容检查的粒度,在不同操作中传递不同的值:
| 操作类型 | check 值 | 含义 | 
|---|---|---|
| 空桶插入 | 0 | 最低优先级的扩容检查 | 
| 链表插入 | 1-7 | 中等优先级的扩容检查 | 
| 树节点插入 | 2 | 标记为树节点 | 
| 替换现有值 | < 0 | 不检查扩容 | 
看似"矛盾"的逻辑解析
            
            
              kotlin
              
              
            
          
          // 第一处:CounterCell 更新成功后
if (check <= 1)
    return;
// 第二处:后续逻辑
if (check >= 0) {
    // 扩容检查逻辑
}
        这实际上是一个精心设计的"过滤网" :
过滤流程分析
            
            
              sql
              
              
            
          
                  check 值分布
     ←--- 负值 --- 0 --- 1 --- 2+ ---→
     不检查    ↓    ↓    ↓    检查
             │    │    │
第一处过滤:   │    │    × (check<=1 时返回)
             │    │
第二处过滤:   × (check<0 跳过)  ✓ (进入扩容检查)
        实际场景分析
场景1:check < 0(如 replace 操作)
        
            
            
              arduino
              
              
            
          
          // 第一处:check <= 1 ? 是,但不会执行到这里
// 因为 replace 操作通常直接返回,不调用 addCount
// 第二处:check >= 0 ? 否,跳过扩容检查
        结果:不检查扩容
场景2:check = 0(空桶插入)
        
            
            
              kotlin
              
              
            
          
          // 第一处:check <= 1 ? 是,但注意逻辑!
if (check <= 1)
    return;  // 这里会返回吗?不一定!
        关键 :这个 return 只在 CounterCell 路径 中生效!
            
            
              kotlin
              
              
            
          
          if ((cs = counterCells) != null || !U.compareAndSetLong(...)) {
    // CounterCell 路径
    if (check <= 1)
        return;  // 在这里,check=0 会返回
}
// 如果是 baseCount CAS 成功,会继续执行
        场景3:check = 1(短链表插入)
        
            
            
              kotlin
              
              
            
          
          // 第一处:check <= 1 ? 是
if (check <= 1)
    return;  // 在 CounterCell 路径中返回
        设计思想:短链表的插入竞争较小,不急于扩容检查
场景4:check >= 2(长链表或树节点)
        
            
            
              arduino
              
              
            
          
          // 第一处:check <= 1 ? 否,继续执行
// 第二处:check >= 0 ? 是,进行扩容检查
        设计思想:冲突较多的桶需要及时检查扩容
🎪 现实世界比喻
医院急诊分诊系统
把 ConcurrentHashMap 比作医院 ,check 参数就像病人病情的紧急程度:
分诊规则:
check < 0:健康体检(不需要急诊处理)check = 0:轻微擦伤(低优先级)check = 1:普通感冒(中等优先级)check ≥ 2:重伤急诊(高优先级)
处理流程:
            
            
              scss
              
              
            
          
          病人到达 → 分诊台评估紧急程度(check)
    ↓
[第一关:CounterCell 通道]
    ↓
轻微病人(check≤1) → 简单处理后就回家(return)
    ↓  
重伤病人(check≥2) → 进入急诊室(扩容检查)
    ↓
[第二关:baseCount 通道]
    ↓  
所有病人(check≥0) → 都进急诊室检查
        uncontended 就像医疗资源竞争
true:医院空闲,直接就诊false:医院繁忙,需要排队或转院
💡 设计哲学深度解析
性能与及时性的平衡
为什么 check <= 1 在 CounterCell 路径中返回?
        
            
            
              kotlin
              
              
            
          
          // 高并发场景下的优化
if (cs = counterCells) != null || !U.compareAndSetLong(...)) {
    // 进入这里说明:要么已有 CounterCell,要么 baseCount CAS 失败
    // 这两种情况都表明系统处于高并发状态
    
    if (check <= 1)  // 对于低冲突的桶
        return;      // 不进行扩容检查,避免在高并发时增加负担
}
        为什么 check >= 0 在 baseCount 路径中检查?
        
            
            
              scss
              
              
            
          
          // 低并发场景下的保证
// 如果能直接 CAS 更新 baseCount 成功,说明并发度低
if (check >= 0) {
    // 即使是最低优先级的检查也执行,因为系统有充足资源
}
        智能的扩容触发策略
这种设计实现了 "自适应扩容检查" :
- 高并发时:只对高冲突的桶进行扩容检查
 - 低并发时:对所有插入都进行扩容检查
 - 无竞争时:最全面的扩容保障
 
实际效果
- 避免过度扩容:在高并发时减少不必要的检查
 - 保证及时扩容:在真正需要时及时触发
 - 资源优化:根据系统负载动态调整策略
 
🔧 代码执行路径总结
路径1:baseCount CAS 成功(低并发)
            
            
              scss
              
              
            
          
          addCount(1L, binCount)
    ↓
baseCount CAS 成功
    ↓
if (check >= 0) → 总是检查扩容
    ↓
可能触发扩容
        路径2:CounterCell 更新成功(高并发)
            
            
              scss
              
              
            
          
          addCount(1L, binCount)  
    ↓
baseCount CAS 失败 → 进入 CounterCell 路径
    ↓
CounterCell CAS 成功
    ↓
if (check <= 1) → 低冲突桶直接返回
    ↓  
if (check >= 2) → 高冲突桶检查扩容
        路径3:CounterCell 初始化/竞争(高并发激烈)
            
            
              scss
              
              
            
          
          addCount(1L, binCount)
    ↓
baseCount CAS 失败 → 进入 CounterCell 路径  
    ↓
CounterCell CAS 失败或需要初始化
    ↓
fullAddCount(x, uncontended) → 复杂处理
    ↓
直接返回(不检查扩容,先解决计数问题)
        这种精妙的设计让 ConcurrentHashMap 能够在各种并发场景下都保持优异的性能!
rs是什么?
rs(resize stamp)是扩容戳,用于唯一标识一次扩容操作:
            
            
              ini
              
              
            
          
          int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
        resizeStamp 计算方法
            
            
              arduino
              
              
            
          
          static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
        rs 的组成:
- 高16位:扩容标识(基于容量n的前导零个数)
 - 低16位:用于记录扩容线程数
 
举例(假设 n=16):
Integer.numberOfLeadingZeros(16)= 27(32-5,因为16=2⁴)1 << (16 - 1)= 0x8000resizeStamp(16)= 27 | 0x8000 = 0x8000 | 0x001B = 0x801B- 左移16位:
rs = 0x801B0000 
🎯 为什么需要这些判断条件
当 sc < 0 时,表示扩容正在进行中,当前线程想要协助扩容。但这些判断确保我们只在"合适的时候"协助:
判断条件逐行解析:
            
            
              ini
              
              
            
          
          if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
    (nt = nextTable) == null || transferIndex <= 0)
    break;
        🔒 四个退出条件的深层原因
1. sc == rs + MAX_RESIZERS - 线程数已达上限
        
            
            
              ini
              
              
            
          
          if (sc == rs + MAX_RESIZERS)
        含义:扩容协助线程数已经达到最大值
数学原理:
rs= 扩容基础戳(如 0x801B0000)MAX_RESIZERS= 最大协助线程数(通常是 2¹⁶-1)sc == rs + MAX_RESIZERS表示:(低16位) = MAX_RESIZERS
为什么需要:
- 防止过多线程参与扩容,造成资源浪费
 - 避免线程间过度竞争,反而降低性能
 
举例:
            
            
              ini
              
              
            
          
          rs = 0x801B0000
MAX_RESIZERS = 0x0000FFFF
rs + MAX_RESIZERS = 0x801BFFFF
如果 sc == 0x801BFFFF,说明已经有65535个线程在协助了
        2. sc == rs + 1 - 扩容即将完成
        
            
            
              ini
              
              
            
          
          if (sc == rs + 1)
        含义:扩容工作马上结束,不需要更多协助
数学原理:
- 初始扩容状态:
sc = rs + 2(1个主导线程 + 1) - 每个协助线程:
sc = sc + 1 - 当 
sc == rs + 1时,表示所有线程都快退出了 
为什么需要:
- 避免在扩容收尾阶段加入,造成混乱
 - 防止新线程刚开始工作,扩容就结束了
 
3. (nt = nextTable) == null - 新表未准备好
        
            
            
              ini
              
              
            
          
          if ((nt = nextTable) == null)
        含义:新的哈希表还没有创建完成
为什么需要:
- 如果 
nextTable为 null,说明扩容还没真正开始 - 或者扩容出现了异常情况
 - 此时协助没有意义,应该退出等待
 
4. transferIndex <= 0 - 任务已分配完
        
            
            
              scss
              
              
            
          
          if (transferIndex <= 0)
        含义:所有迁移任务都已经被分配完毕
背景知识:
transferIndex:从后往前分配迁移任务的范围指针- 初始值 = 旧表长度 n
 - 每个线程领取一段任务:
[transferIndex-stride, transferIndex) 
为什么需要:
- 如果 
transferIndex <= 0,说明所有桶都已经分配了迁移任务 - 没有剩余工作需要协助,应该退出
 
✅ 通过检查后的协助逻辑
            
            
              scss
              
              
            
          
          if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
    transfer(tab, nt);
        逻辑:
- CAS 增加协助线程计数 :
sc → sc + 1 - 参与实际迁移工作 :调用 
transfer(tab, nt) 
CAS 的重要性:确保线程安全地增加计数器,防止并发问题
🎪 形象比喻:建筑工地协作
把扩容比作建筑工地搬迁:
场景设定
- 旧工地 = 当前哈希表
 - 新工地 = nextTable
 - 工头 = 主导扩容的线程
 - 工人 = 协助扩容的线程
 - 任务板 = transferIndex
 - 工号牌 = sizeCtl 的低16位
 
四个退出条件的现实对应
- 
sc == rs + MAX_RESIZERS👥- "工地已经招了65535个工人,人满为患,别再来了!"
 
 - 
sc == rs + 1🏁- "搬迁工作已经完成99%,现在来帮忙反而添乱"
 
 - 
(nt = nextTable) == null🏗️- "新工地还没建好呢,来了也没活干"
 
 - 
transferIndex <= 0📋- "任务板上已经没有待分配的工作了"
 
 
合格的协助流程
            
            
              yaml
              
              
            
          
          工人A来到工地 → 查看情况:
✅ 工地还在搬迁中(sc < 0)
✅ 工人数量没满(不是 rs + MAX_RESIZERS)
✅ 不是收尾阶段(不是 rs + 1)  
✅ 新工地已建好(nextTable != null)
✅ 还有未分配的任务(transferIndex > 0)
→ 去管理处登记领工号(CAS: sc → sc+1)
→ 开始实际搬迁工作(transfer)
        💡 设计哲学总结
"恰到好处"的协作
- 不早不晚:在需要的时候协助,不需要的时候退出
 - 不多不少:控制协助线程数量,避免过度竞争
 - 不慌不忙:有序参与,避免状态混乱
 
状态机的精确控制
通过 sizeCtl 这一个变量,同时维护:
- 扩容标识(高16位):确保是同一轮扩容
 - 线程计数(低16位):精确控制参与线程数
 - 整体状态:正数=阈值,负数=扩容中
 
自我保护机制
这些判断条件形成了完整的自我保护:
- 资源保护:防止线程爆炸
 - 状态保护:防止错误参与
 - 进度保护:防止干扰收尾工作
 
这种精妙的设计让 ConcurrentHashMap 在高并发扩容时既能充分利用多核优势,又能避免过度竞争和资源浪费!
sc = rs + 2的原因
这确实不是巧合,而是 精心设计的数学关系!让我深入解释这个设计的巧妙之处。
🔢 数字关系的数学原理
rs 的结构分析
        
            
            
              bash
              
              
            
          
          int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;  // RESIZE_STAMP_SHIFT = 16
        rs 的特点:
- 高16位包含扩容标识信息
 - 低16位全为0(因为左移了16位)
 
举例(假设 n=16):
            
            
              scss
              
              
            
          
          resizeStamp(16) = 0x801B     // 高16位: 0x801B, 低16位: 0x0000
rs = 0x801B << 16 = 0x801B0000  // 高16位: 0x801B, 低16位: 0x0000
        🎯 rs + 2 的深层含义
状态编码设计
在 ConcurrentHashMap 中,sizeCtl 在扩容期间被设计为:
            
            
              ini
              
              
            
          
          sizeCtl = rs + (1 + 协助线程数)
        具体分解:
rs + 1= 扩容完成或即将完成的状态rs + 2= 初始状态(1个主导线程 + 1)rs + 3= 有2个线程在扩容rs + 4= 有3个线程在扩容- ...
 rs + MAX_RESIZERS= 达到最大线程数
为什么是 +2 而不是 +1?
        
            
            
              kotlin
              
              
            
          
          // 第一个线程发起扩容
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
    transfer(tab, null);
        关键洞察 :+2 表示 "1个主导线程 + 1个计数基准"
数学关系:
- 实际扩容线程数 = 
(sizeCtl - rs) - 1 - 当 
sizeCtl = rs + 2时:实际线程数 =(rs + 2 - rs) - 1 = 1 
🔍 状态转换的完整生命周期
扩容状态演进
            
            
              ini
              
              
            
          
          正常状态: sizeCtl = 12 (阈值)
    ↓
开始扩容: sizeCtl = rs + 2 (1个线程)
    ↓
线程A协助: sizeCtl = rs + 3 (2个线程)  
    ↓
线程B协助: sizeCtl = rs + 4 (3个线程)
    ↓
...
    ↓
完成前: sizeCtl = rs + 1 (0个线程,即将完成)
    ↓
完成: sizeCtl = 24 (新阈值)
        代码中的状态检查
            
            
              ini
              
              
            
          
          if (sc == rs + MAX_RESIZERS)    // 线程数达上限
if (sc == rs + 1)               // 扩容即将完成
        💡 设计精妙之处
1. 统一的数学框架
            
            
              scss
              
              
            
          
          // 所有状态检查都基于同一个数学关系
线程数 = (sc - rs) - 1
// 因此可以统一判断:
if (线程数 >= MAX_RESIZERS)  → if (sc == rs + MAX_RESIZERS)
if (线程数 == 0)             → if (sc == rs + 1)
        2. 避免特殊值冲突
如果使用 rs + 1 作为初始值:
- 完成状态就无法用 
rs + 1表示 - 需要另外定义特殊值,增加复杂度
 
3. 自然的计数增长
            
            
              kotlin
              
              
            
          
          // 线程协助时的操作非常自然
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
        - 不需要特殊处理初始状态
 - 每个线程简单地 
+1,数学关系自动保持 
4. 清晰的状态边界
rs + 1:明确的完成边界rs + 2:明确的开始边界- 中间状态:线性的线程计数
 
🎪 现实世界比喻
团队项目的版本号管理
把扩容比作一个软件开发项目:
            
            
              ini
              
              
            
          
          版本号 = 项目代号 + 参与人数
rs = "CHM-2024" (项目基础代号)
rs + 1 = "CHM-2024-FINAL"  // 项目完成,准备发布
rs + 2 = "CHM-2024-TEAM1"  // 项目启动,1个核心开发者
rs + 3 = "CHM-2024-TEAM2"  // 第2个开发者加入
...
rs + MAX = "CHM-2024-TEAMMAX" // 团队满员
        工作流程:
- 项目经理 (主导线程)创建项目:
rs + 2 - 开发者A 加入:检查是否 
当前版本 == FINAL?不是 →版本+1 - 开发者B 加入:检查是否 
当前版本 == TEAMMAX?不是 →版本+1 - 项目完成 :版本回到 
FINAL状态 
🔧 实际代码验证
让我们验证这个设计的一致性:
            
            
              kotlin
              
              
            
          
          // 初始状态:1个线程在扩容
sizeCtl = rs + 2
// 线程协助成功
U.compareAndSetInt(this, SIZECTL, sc, sc + 1)  // sc = rs + 2 → rs + 3
// 现在有2个线程:(rs + 3 - rs) - 1 = 2
// 线程退出时
U.compareAndSetInt(this, SIZECTL, sc, sc - 1)  // sc = rs + 3 → rs + 2  
// 回到1个线程
// 最后一个线程退出
U.compareAndSetInt(this, SIZECTL, sc, sc - 1)  // sc = rs + 2 → rs + 1
// 现在有0个线程,扩容完成
        🏆 总结:这不是巧合,而是精妙设计
sc = rs + 2 的选择体现了 "简单即美" 的设计哲学:
- 数学优雅:用简单的算术关系表达复杂的状态机
 - 操作一致 :所有状态转换都通过简单的 
+1/-1完成 - 边界清晰:开始和结束状态有明确的数学定义
 - 并发安全:CAS 操作天然适合这种线性增长
 
这种设计让复杂的多线程协作变得可预测、可推理、可维护,正是 ConcurrentHashMap 高并发能力的基石之一!
所以,这不是"巧合",而是计算机科学和软件工程的精妙结合!
binCount 的作用详解
在 putVal 方法中,binCount 是一个非常重要的变量,它有多重作用,让我详细解释:
📋 binCount 的基本含义
binCount 表示当前桶的节点数量(在插入新节点之前),具体含义因桶类型而异:
- 链表:链表的长度(从0开始计数)
 - 红黑树:固定值 2(作为标记值)
 - 空桶:值为 0
 
🎯 binCount 的三大核心作用
1. 树化判断依据(最主要作用)
            
            
              scss
              
              
            
          
          // 在链表遍历插入时
for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        // 树化判断:链表长度达到阈值
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
            treeifyBin(tab, hash);
        break;
    }
    // ... 其他逻辑
}
        树化条件:
binCount >= TREEIFY_THRESHOLD - 1(即链表长度 ≥ 8)- 为什么要 
-1?因为 binCount 从0开始计数 
举例:
            
            
              css
              
              
            
          
          链表:A → B → C → D → E → F → G → H
binCount 变化:0→1→2→3→4→5→6→7
当 binCount=7 时,插入第9个节点,触发树化
        2. 扩容检查的触发条件
            
            
              scss
              
              
            
          
          // 在 putVal 方法最后
addCount(1L, binCount);
        binCount 作为第二个参数传递给 addCount,控制扩容检查的粒度:
| binCount 值 | 含义 | 是否检查扩容 | 
|---|---|---|
| 0 | 空桶插入 | 是 | 
| 1-7 | 短链表插入 | 是 | 
| ≥ 8 | 长链表或树节点 | 是 | 
设计思想:无论桶的长度如何,每次插入都会检查是否需要扩容,但检查的紧迫性不同。
3. 区分桶类型的标记
            
            
              java
              
              
            
          
          else if (f instanceof TreeBin) {
    // 红黑树处理
    binCount = 2;  // 固定标记值
}
        对于红黑树,binCount 被固定设为 2,这有几个目的:
- 标记作用:表示这是树节点,不是链表
 - 避免误树化:值2 < 8,不会重复触发树化
 - 兼容性:保持方法调用的一致性
 
🔍 实际代码流程分析
场景1:空桶插入
            
            
              scss
              
              
            
          
          if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);
else {
    // 非空桶处理
}
// 调用 addCount
addCount(1L, 0);  // binCount=0 表示空桶插入
        场景2:链表插入
            
            
              ini
              
              
            
          
          for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        p.next = newNode(hash, key, value, null);
        if (binCount >= TREEIFY_THRESHOLD - 1)  // 树化检查
            treeifyBin(tab, hash);
        break;
    }
    if (e.hash == hash &&
        ((k = e.key) == key || (key != null && key.equals(k))))
        break;  // 键已存在
    p = e;
}
// binCount 是最终的链表长度
addCount(1L, binCount);
        场景3:树节点插入
            
            
              ini
              
              
            
          
          else if (f instanceof TreeBin) {
    Node<K,V> p;
    binCount = 2;  // 固定标记值
    if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
        oldVal = p.val;
        if (!onlyIfAbsent)
            p.val = value;
    }
}
addCount(1L, binCount);  // binCount=2
        🎪 形象比喻:超市收银台
把 ConcurrentHashMap 比作超市 ,桶比作收银台:
binCount 就是收银台排队人数
- 
空收银台(binCount=0)
- 新顾客直接结账
 - 通知经理:"现在有1个顾客了"
 
 - 
短队伍(binCount=1-7)
- 顾客排队等候
 - 通知经理:"现在有X个顾客在排队"
 
 - 
长队伍(binCount=8)
- 经理看到:"这个收银台人太多了!"
 - 决定:"升级为快速收银通道!"(链表→红黑树)
 
 - 
快速通道(binCount=2)
- 已经是快速通道了,标记为"特殊通道"
 - 不再重复升级,但继续统计顾客数量
 
 
💡 设计哲学思考
为什么用 binCount 而不用实际长度?
- 
性能优化:
- 遍历时自然计数,无需额外计算长度
 - 避免对长链表的重复遍历
 
 - 
状态融合:
- 一个变量同时承载长度信息和类型标记
 - 简化方法签名和逻辑判断
 
 - 
渐进式决策:
- 在插入过程中实时判断,避免事后分析
 - 及时触发树化或扩容,防止性能恶化
 
 
binCount 的智能之处
- 动态感知:实时反映桶的负载情况
 - 多重职责:既是计数器,也是决策依据
 - 阈值触发:在关键时刻(长度=8)自动升级数据结构
 
这个精巧的设计让 ConcurrentHashMap 能够自适应地调整内部结构,在保持高性能的同时优雅地处理哈希冲突!
Transfer逻辑
            
            
              ini
              
              
            
          
           private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
     int n = tab.length, stride;
     //stride,每个线程处理的桶的数量,根据NCPU计算
     //但是即使CPU很多,每个线程也应该有一个保底的数量,不会太少,不至于少于16个。
     if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
         stride = MIN_TRANSFER_STRIDE; // subdivide range
     if (nextTab == null) {            // initiating
         try {
             @SuppressWarnings("unchecked")
             //扩容,表的大小直接翻倍
             Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
             nextTab = nt;
             //Out of memery,把sizeCtl设置成最大数,所以就不会再触发transfer的逻辑了,即使触发了,也没有办法了。
         } catch (Throwable ex) {      // try to cope with OOME
             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; // to ensure sweep before committing nextTab
     //看似初始值i=0,但其实i是倒着走的,它的值表示当前正在工作的槽,  i = nextIndex - 1;
     for (int i = 0, bound = 0;;) {
         Node<K,V> f; int fh;
         //任务分配逻辑,不管怎样,不管哪一个if,反正至少要进入到其中一个if的逻辑中,advance才能变成false。
         while (advance) {
            
             int nextIndex, nextBound;
             //任务还能继续干,淦!
             //这里为什么多出来一个finishing呢?因为后来可以回出现循环检查的情况,
             //进入这个逻辑,直接advance=false,就不需要后面的任务判断,然后每一个都直接走出while逻辑,--i
            //会直接走到 already processed的部分。
             if (--i >= bound || finishing)
                 advance = false;
             //干完了,应该就是0
             //transferIndex是一个成员变量
             //初始化的线程会把它设置成n,
             else if ((nextIndex = transferIndex) <= 0) {
                 i = -1;
                 advance = false;
             }
              //nextIndex,开始处理的index+1
             //nextBound,结束处理的位置。
             //思考一下,为什么CAS的时候使用了nextIndex,而不是直接使用transferIndex?
             else if (U.compareAndSetInt
                      (this, TRANSFERINDEX, nextIndex,
                       nextBound = (nextIndex > stride ?
                                    nextIndex - stride : 0))) {
                 bound = nextBound;
                 i = nextIndex - 1;
                 advance = false;
             }
         }
         //虽然我不能理解为什么需要这样写?
         //i >= n, 其实就是在完成所有任务的时候, i = n; // recheck before commit,再重新检查一下
         //这里或许会出现并发问题,检查一下,看有没有遗漏的槽。
         if (i < 0 || i >= n || i + n >= nextn) {
             int sc;
             if (finishing) {
                 nextTable = null;
                 table = nextTab;
                 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; // recheck before commit
             }
         }
         else if ((f = tabAt(tab, i)) == null)
             advance = casTabAt(tab, i, null, fwd);
         else if ((fh = f.hash) == MOVED)
             advance = true; // already processed
         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");
                 }
             }
         }
     }
 }
        ConcurrentHashMap.transfer() 方法深度解析与可视化
这是 ConcurrentHashMap 中最核心、最复杂的扩容迁移方法。让我通过文字 + 可视化图表的方式详细解析。
📊 方法整体架构
方法三个阶段:
- 初始化阶段:计算步长、创建新数组
 - 任务分配阶段:多线程协作分配迁移任务
 - 数据迁移阶段:实际移动节点到新数组
 
🔧 第一阶段:初始化准备
1.1 步长计算(Stride Calculation)
            
            
              ini
              
              
            
          
           int n = tab.length, stride;
 if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
     stride = MIN_TRANSFER_STRIDE;
        可视化计算过程:
            
            
              ini
              
              
            
          
           假设:n=64, NCPU=4, MIN_TRANSFER_STRIDE=16
 
 计算步骤:
 n >>> 3 = 64 / 8 = 8
 8 / NCPU = 8 / 4 = 2
 2 < 16 → stride = 16
 
 最终:每个线程处理16个桶
        1.2 新表初始化
            
            
              ini
              
              
            
          
          if (nextTab == null) {            // 第一个发起扩容的线程
    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];  // 容量翻倍
    nextTab = nt;
    nextTable = nextTab;
    transferIndex = n;  // 从后往前迁移,起始位置=旧表长度
}
        内存状态变化:
            
            
              scss
              
              
            
          
          扩容前:
table    → [0][1][2]...[63] (长度64)
nextTable → null
扩容后:
table    → [0][1][2]...[63] (长度64)  
nextTable → [0][1][2]...[127] (长度128)
transferIndex = 64
        🎯 第二阶段:任务分配机制
2.1 核心循环结构
            
            
              ini
              
              
            
          
          boolean advance = true;
boolean finishing = false;
for (int i = 0, bound = 0;;) {
    Node<K,V> f; int fh;
    while (advance) {
        // 任务分配逻辑
    }
    // 迁移逻辑
}
        2.2 任务分配状态机
            
            
              ini
              
              
            
          
          while (advance) {
    int nextIndex, nextBound;
    if (--i >= bound || finishing)        // 情况1:当前段内继续处理
        advance = false;
    else if ((nextIndex = transferIndex) <= 0) { // 情况2:所有任务已完成
        i = -1;
        advance = false;
    }
    else if (U.compareAndSetInt          // 情况3:获取新任务段
             (this, TRANSFERINDEX, nextIndex,
              nextBound = (nextIndex > stride ? nextIndex - stride : 0))) {
        bound = nextBound;
        i = nextIndex - 1;
        advance = false;
    }
}
        任务分配可视化:
            
            
              css
              
              
            
          
          初始状态:n=64, stride=16, transferIndex=64
时间线:
┌─────────┬────────────┬──────────┬─────────┬─────────┐
│  线程   │  操作顺序   │ 获取区间  │ bound   │   i     │
├─────────┼────────────┼──────────┼─────────┼─────────┤
│ 线程A   │    第1步    │ [48,64)  │ 48      │ 63      ││ 线程B   │    第2步    │ [32,48)  │ 32      │ 47      │  │ 线程C   │    第3步    │ [16,32)  │ 16      │ 31      ││ 线程D   │    第4步    │ [0,16)   │ 0       │ 15      │└─────────┴────────────┴──────────┴─────────┴─────────┘
        图形化表示任务分配:
            
            
              less
              
              
            
          
          旧数组索引: 0   1   2 ... 15  16  17 ... 31  32  33 ... 47  48  49 ... 63
            ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
任务分配:    │线程D │      线程C      │      线程B      │      线程A      │
            └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
        🚀 第三阶段:数据迁移逻辑
3.1 迁移完成检查
            
            
              ini
              
              
            
          
          if (i < 0 || i >= n || i + n >= nextn) {
    // 扩容完成处理
    if (finishing) {
        nextTable = null;
        table = nextTab;          // 切换主表引用
        sizeCtl = (n << 1) - (n >>> 1);  // 新阈值 = 1.5n
        return;
    }
    // 当前线程退出扩容
    if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
            return;  // 不是最后一个线程
        finishing = advance = true;  // 最后一个线程,重新扫描
        i = n; // 从最后开始检查
    }
}
        线程退出可视化:
            
            
              ini
              
              
            
          
          初始:sizeCtl = rs + 4 (4个线程参与)
线程A完成:sizeCtl = rs + 3
线程B完成:sizeCtl = rs + 2  
线程C完成:sizeCtl = rs + 1
线程D(最后一个)完成:切换table引用,清理状态
        3.2 空桶处理
            
            
              ini
              
              
            
          
          else if ((f = tabAt(tab, i)) == null)
    advance = casTabAt(tab, i, null, fwd);  // 直接标记为ForwardingNode
        空桶迁移过程:
            
            
              css
              
              
            
          
          迁移前:
tab[i] = null
迁移后:  
tab[i] = ForwardingNode → nextTable
        3.3 链表迁移(核心算法)
            
            
              ini
              
              
            
          
          synchronized (f) {
    if (tabAt(tab, i) == f) {
        if (fh >= 0) {
            // 1. 寻找最后一段连续相同位的节点
            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;
                }
            }
            
            // 2. 构建两个链表
            // ... 详细逻辑
        }
    }
}
        链表迁移优化算法图解:
步骤1:寻找 lastRun
            
            
              less
              
              
            
          
          原始链表:A → B → C → D → E → F → G → H
hash & n 计算:
A:0, B:0, C:0, D:1, E:1, F:1, G:1, H:1
        ↑
     lastRun (从这里开始都是连续的1)
结果:lastRun = D
        步骤2:链表拆分优化
            
            
              css
              
              
            
          
          第一次遍历(lastRun之前):
低位链表:C → B → A  (头插法)
高位链表:不需要创建新节点
第二次遍历(lastRun及之后):
直接复用:D → E → F → G → H
最终:
低位链表:C → B → A
高位链表:D → E → F → G → H
        为什么这样优化?
- 减少对象创建:lastRun 之后的节点直接复用
 - 保持顺序:虽然前半部分头插法,但并发场景下顺序本身就不保证
 
步骤3:放置到新表
            
            
              scss
              
              
            
          
          setTabAt(nextTab, i, ln);      // 原位置:i
setTabAt(nextTab, i + n, hn);  // 新位置:i + 旧容量
setTabAt(tab, i, fwd);         // 标记为已迁移
        位置计算原理:
            
            
              bash
              
              
            
          
          因为新容量是2的幂,hash & (2n-1) 的结果:
- 等于 hash & (n-1)        → 留在位置 i
- 等于 hash & (n-1) + n    → 移动到位置 i + n
        3.4 红黑树迁移
            
            
              ini
              
              
            
          
          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) {
        // 根据 (h & n) 分为低位和高位链表
    }
    
    // 判断是否需要树化或链化
    ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) : new TreeBin<K,V>(lo);
    hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) : new TreeBin<K,V>(hi);
}
        树迁移决策逻辑:
            
            
              perl
              
              
            
          
          节点数判断:
lc ≤ 6 → 链化低位链表
hc ≤ 6 → 链化高位链表  
否则 → 树化
特殊情况:如果一边为0,复用原树
        🎪 完整迁移过程可视化示例
场景:容量16→32的扩容
初始状态:
            
            
              ini
              
              
            
          
          旧表 (n=16):
索引:  0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15
      [A] [ ] [C] [D] [E] [F] [ ] [G] [H] [I] [J] [K] [L] [M] [N] [O]
              ↓
            B→C
新表 (nextn=32): [空表]
transferIndex = 16
        迁移过程:
第1步:线程A获取任务 [12,16)
            
            
              makefile
              
              
            
          
          处理桶15: O → 新表位置15或31
处理桶14: N → 新表位置14或30  
处理桶13: M → 新表位置13或29
处理桶12: L → 新表位置12或28
        第2步:线程B获取任务 [8,12)
            
            
              makefile
              
              
            
          
          处理桶11: K → 新表位置11或27
处理桶10: J → 新表位置10或26
处理桶9:  I → 新表位置9或25
处理桶8:  H → 新表位置8或24
        第3步:处理链表桶2
            
            
              ini
              
              
            
          
          桶2: B → C
计算:
B.hash & 16 = 0 → 低位
C.hash & 16 = 0 → 低位
结果:
新表[2] = B → C
标记旧表[2] = ForwardingNode
        最终状态:
            
            
              makefile
              
              
            
          
          旧表: 所有桶都是ForwardingNode
新表: 数据均匀分布在32个桶中
table引用指向新表
        💡 设计亮点总结
1. 并行迁移
- 多线程同时处理不同区段
 - 无锁任务分配,避免竞争
 
2. 优化算法
- lastRun优化减少对象创建
 - 链表保持部分顺序
 
3. 状态安全
- 精确的线程计数控制
 - 安全的表切换机制
 
4. 渐进式
- 迁移过程中可继续读写
 - ForwardingNode引导访问新表
 
5. 资源控制
- 根据CPU数量动态调整步长
 - 防止过多线程参与造成竞争
 
这个精妙的迁移算法是 ConcurrentHashMap 高并发性能的基石,通过巧妙的并行设计和状态管理,实现了真正无阻塞的扩容!
辅助机制
同步扩容
在putVal()方法中有这样一行:
            
            
              ini
              
              
            
          
          else if ((fh = f.hash) == MOVED)
    tab = helpTransfer(tab, f);
        这是 ConcurrentHashMap 中的 协助扩容方法(helpTransfer),当线程在操作时发现当前桶正在迁移,会调用此方法来协助完成扩容工作。
前因是什么呢?为什么会出现这样一种情况呢?
当然是,已经有了线程正在执行迁移的逻辑,迁移的时候会对node上锁,迁移结束之后,会把node设置成一个特殊的fwd
            
            
              scss
              
              
            
          
                     setTabAt(tab, i, fwd);
        之后其他节点过来的时候,就会进入这个if的逻辑。
方法作用
            
            
              swift
              
              
            
          
          final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        - 目的:让当前线程协助完成哈希表的扩容迁移工作
 - 触发条件 :当操作过程中遇到 
ForwardingNode(转发节点) 
代码逻辑分析
1. 条件检查
            
            
              yaml
              
              
            
          
          if (tab != null && (f instanceof ForwardingNode) &&
    (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        - 检查表不为空
 - 当前节点是 
ForwardingNode(表示该桶正在迁移) - 获取新表 
nextTable不为空 
一般来说,都会进入这个逻辑,想不到什么时候不为true。
2. 计算扩容标识
            
            
              ini
              
              
            
          
          int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
        resizeStamp(tab.length):生成与当前容量相关的扩容戳- 左移 
RESIZE_STAMP_SHIFT(16位)得到扩容状态的基础值 
            
            
              arduino
              
              
            
          
          /**
 * Returns the stamp bits for resizing a table of size n.
 * Must be negative when shifted left by RESIZE_STAMP_SHIFT.
 */
static final int resizeStamp(int n) {
    return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
        逻辑也挺简单的,最低位是前导零,接着在第十六位上一个1,如果再右移16位,也就是32位上一个1,又是优雅的位运算。
3. 循环协助迁移
            
            
              ini
              
              
            
          
          while (nextTab == nextTable && table == tab &&
       (sc = sizeCtl) < 0) {
        循环条件:
- 新表未改变
 - 旧表未改变
 sizeCtl < 0(表示扩容仍在进行中)
4. 退出条件检查
            
            
              ini
              
              
            
          
          if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
    transferIndex <= 0)
    break;
        退出条件:
sc == rs + MAX_RESIZERS:协助线程数已达上限sc == rs + 1:扩容即将完成transferIndex <= 0:所有迁移任务已完成
5. CAS 尝试参与扩容
            
            
              kotlin
              
              
            
          
          if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
    transfer(tab, nextTab);
    break;
}
        - 通过 CAS 将 
sizeCtl加1(增加一个协助线程) - 成功后调用 
transfer执行实际的迁移工作 
关键设计思想
1. 协作式扩容
- 多线程并行迁移:多个线程可以同时协助完成数据迁移
 - 工作窃取:线程完成自己的迁移任务后可以协助其他线程
 
2. 状态控制机制
sizeCtl 的不同值表示不同状态:
- 正数:扩容阈值
 - -1:表正在初始化
 - 其他负数:高16位是扩容标识,低16位是参与扩容的线程数
 
3. 转发节点(ForwardingNode)
            
            
              scala
              
              
            
          
          static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    // ...
}
        - 特殊标记节点,表示该桶已迁移完成
 - 包含指向新表的引用
 
工作流程示例
- 线程A 在 put 操作时发现桶节点是 
ForwardingNode - 线程A 调用 
helpTransfer检查是否可以协助 - 如果条件满足,线程A通过 CAS 注册为协助线程
 - 线程A 调用 
transfer参与数据迁移 - 迁移完成后,操作在新表上继续执行
 
性能优势
- 避免操作阻塞 :读操作遇到 
ForwardingNode可以直接转到新表 - 加速扩容:多线程并行迁移大幅缩短扩容时间
 - 负载均衡:自动根据系统负载调整迁移速度
 
这种设计体现了 ConcurrentHashMap 的"人人为我,我为人人"思想:每个线程在需要时都能得到其他线程的协助,同时自己也会在可能时协助他人,共同完成繁重的扩容任务。
transferIndex 的 CAS 操作深度解析
        
            
            
              ini
              
              
            
          
                  //transferIndex是一个成员变量
        //初始化的线程会把它设置成n,
        else if ((nextIndex = transferIndex) <= 0) {
            i = -1;
            advance = false;
        }
         //nextIndex,开始处理的index+1
        //nextBound,结束处理的位置。
        //思考一下,为什么CAS的时候使用了nextIndex,而不是直接使用transferIndex?
        else if (U.compareAndSetInt
                 (this, TRANSFERINDEX, nextIndex,
                  nextBound = (nextIndex > stride ?
                               nextIndex - stride : 0))) {
            bound = nextBound;
            i = nextIndex - 1;
            advance = false;
        }
        你问到了一个非常关键的设计点!让我详细解释为什么需要 nextIndex 局部变量,以及 transferIndex 的 volatile 特性如何影响这个设计。
🔍 transferIndex 的 volatile 特性
是的,transferIndex 是 volatile 变量!
        
            
            
              java
              
              
            
          
          // 在 ConcurrentHashMap 中的定义
private transient volatile int transferIndex;
        volatile 的作用:
- 内存可见性 :所有线程都能立即看到 
transferIndex的最新值 - 禁止指令重排序:保证操作的顺序性
 
🎯 为什么需要 nextIndex 局部变量?
1. 竞态条件保护
问题场景:
            
            
              kotlin
              
              
            
          
          // 错误写法:直接在 CAS 中使用 transferIndex
else if (U.compareAndSetInt(this, TRANSFERINDEX, transferIndex, 
                           transferIndex - stride)) {
    // 潜在问题:transferIndex 可能已被其他线程修改!
}
        竞态风险:
            
            
              ini
              
              
            
          
          时间线:
1. 线程A读取 transferIndex = 64
2. 线程B修改 transferIndex = 48
3. 线程A执行 CAS(TRANSFERINDEX, 64, 48) → 失败!
因为实际值已经是48,不是64了
        2. CAS 操作的原子性要求
CAS 操作需要 "比较-交换" 的原子性:
            
            
              kotlin
              
              
            
          
          U.compareAndSetInt(this, TRANSFERINDEX, expectedValue, newValue)
        expectedValue:期望的旧值(必须与内存中当前值匹配)newValue:要设置的新值
使用局部变量的正确模式:
            
            
              ini
              
              
            
          
          int nextIndex = transferIndex;  // 快照当前值
// 基于快照值计算
int nextBound = (nextIndex > stride) ? nextIndex - stride : 0;
// CAS 使用快照值作为期望值
if (U.compareAndSetInt(this, TRANSFERINDEX, nextIndex, nextBound)) {
    // 成功:基于快照值设置局部变量
    bound = nextBound;
    i = nextIndex - 1;
    advance = false;
}
        🔄 完整的线程安全逻辑
代码解析:
            
            
              ini
              
              
            
          
          else if (U.compareAndSetInt
         (this, TRANSFERINDEX, nextIndex,          // 期望值:快照的nextIndex
          nextBound = (nextIndex > stride ?        // 新值:计算出的下界
                       nextIndex - stride : 0))) {
    bound = nextBound;    // 任务段的下界
    i = nextIndex - 1;    // 从后往前处理,起始索引
    advance = false;      // 退出任务分配循环,开始处理
}
        执行流程:
- 
读取快照 :
nextIndex = transferIndex(volatile 读) - 
计算新值 :
nextBound = nextIndex - stride - 
原子更新 :CAS 尝试将
transferIndex从nextIndex更新为nextBound - 
结果处理:
- 成功 :当前线程获得任务段 
[nextBound, nextIndex) - 失败:循环重试(其他线程已修改)
 
 - 成功 :当前线程获得任务段 
 
🎪 现实世界比喻
超市排队取号系统
把迁移任务分配比作银行/医院的取号系统:
transferIndex = 总服务台
        
            
            
              arduino
              
              
            
          
          volatile int transferIndex = 100;  // 当前可服务的最大号码
        nextIndex = 你看到的号码牌
        
            
            
              ini
              
              
            
          
          // 你走到服务台前
int nextIndex = transferIndex;  // 看到"当前服务到100号"
// 你决定取10个号码(stride=10)
int nextBound = nextIndex - 10;  // 你要90-100号
// 向服务员申请
if (CAS(当前号码=100, 期望改为=90)) {
    // 成功!你获得了90-100号的处理权
    bound = 90;  // 从90号开始
    i = 99;      // 先处理99号(从后往前)
}
        如果不用局部变量的灾难场景:
            
            
              arduino
              
              
            
          
          你: "我要100-90号"
服务员: "好的...(正在处理)"
同时,另一个人: "我要100-90号" 
服务员: "混乱!到底给谁?"
        使用局部变量的正确场景:
            
            
              arduino
              
              
            
          
          你: "我看到现在是100号,我要100-90号"
服务员: "检查现在确实是100号 → 改为90号 → 成功!"
另一个人: "我看到现在是90号,我要90-80号"  
服务员: "检查现在确实是90号 → 改为80号 → 成功!"
        💡 设计精妙之处
1. 乐观锁思想
            
            
              objectivec
              
              
            
          
          // 不加锁,而是"先尝试,失败了重试"
do {
    int nextIndex = transferIndex;  // 读取最新值
    // 计算、尝试CAS...
} while (CAS失败);
        2. 无锁并行
- 多个线程可以同时尝试获取任务段
 - 只有成功的线程继续工作,失败的重新尝试
 - 避免线程阻塞,提高CPU利用率
 
3. 任务分配的原子性
            
            
              ini
              
              
            
          
          // 这三个操作在CAS成功后是原子的:
bound = nextBound;    // 1. 设置下界
i = nextIndex - 1;    // 2. 设置起始索引  
advance = false;      // 3. 退出分配循环
        4. 从后往前的处理顺序
            
            
              ini
              
              
            
          
          i = nextIndex - 1;  // 从区间的最大值开始处理
        好处:减少与正常put操作的冲突(新元素通常插入到前面)
🔧 并发场景分析
场景:4个线程竞争任务
初始状态 :transferIndex = 64, stride = 16
            
            
              ini
              
              
            
          
          时间线:
t0: 线程A读取 nextIndex=64, 计算 nextBound=48
t1: 线程B读取 nextIndex=64, 计算 nextBound=48  
t2: 线程A CAS成功: transferIndex=64→48
t3: 线程B CAS失败: 期望64, 实际48 → 重新循环
t4: 线程B读取 nextIndex=48, 计算 nextBound=32
t5: 线程B CAS成功: transferIndex=48→32
        结果:
- 线程A:处理区间 [48, 64)
 - 线程B:处理区间 [32, 48)
 - 没有重叠,完美并行!
 
🚨 如果不这样设计的后果
错误实现1:直接使用 transferIndex
        
            
            
              kotlin
              
              
            
          
          // 错误!transferIndex可能被修改
else if (U.compareAndSetInt(this, TRANSFERINDEX, 
                           transferIndex, transferIndex - stride))
        问题:CAS 的期望值可能已经过时,导致永远失败或错误成功
错误实现2:没有 volatile
            
            
              java
              
              
            
          
           private transient int transferIndex;  // 非volatile
        问题:
- 线程间看不到最新值
 - 可能出现多个线程处理相同区间
 - 数据一致性被破坏
 
✅ 总结
nextIndex 局部变量的使用体现了 "读取-计算-比较交换" 的标准无锁编程模式:
- volatile 读取:获取共享状态的最新快照
 - 本地计算:基于快照计算新值(无副作用)
 - 原子更新:CAS 确保状态变更的原子性
 - 失败重试:乐观锁的典型模式
 
这种设计确保了:
- 🏃 高并发:多线程无锁协作
 - 🔒 线程安全:不会出现任务重叠或遗漏
 - ⚡ 高性能:避免线程阻塞
 - 🎯 精确控制:任务段分配准确无误
 
这正是 ConcurrentHashMap 能够在高并发环境下保持高性能的关键技术之一!
等一下,volatile不是重点,volatile只是一个安全性的补充罢了,重点在于,这里需要一个快照进行CAS,才能实现乐观锁。所以直接上transferIndex,乐观锁就失效了,因为CAS永远通过......
位运算
在ConcurrentHashMap中,多处使用到位运算。
我不是科班出生,大学上的计算机基础又确实太基础了,当时也太傻。不过,仔细学习,感觉位运算实在很优雅。一直都很有趣,如果我时间多一点就好了,就可以再仔细钻研了。
首先回答一个问题:为什么补码是反码+1?就如同,为什么时针倒退两小时和时针正走十小时的效果一样。减去一个数,等于加上这个数距离那个数的距离。那个数,这里具体取决于有多少位,或者是232,或者是264,因为最高位是符号位。
我现在理解为什么补码是反码+1
问题1:补码为什么等于反码+1
这涉及到计算机中负数的表示方法,让我从数学原理和设计逻辑来解释:
数学原理
对于一个 n 位二进制系统:
- 模数 = 2ⁿ
 - 补码的设计目标是:x + (-x) = 2ⁿ(在模运算下等于0)
 
推导过程
以 8 位系统为例(n=8,模数=256):
对于负数 -x(x>0):
- 
反码 = (2ⁿ - 1) - x
- 因为 2ⁿ - 1 是全 1(11111111)
 - 减去 x 就是按位取反
 
 - 
补码 = 2ⁿ - x
- 根据补码定义:x + 补码 = 2ⁿ
 
 - 
关系推导:
ini补码 = 2ⁿ - x = [(2ⁿ - 1) - x] + 1 = 反码 + 1 
实际例子
求 -5 的补码(8位):
- +5 原码:00000101
 - 反码:11111010(按位取反)
 - 补码:11111011(反码+1)
 
验证:5 + (-5) = 00000101 + 11111011 = 100000000 = 256 ≡ 0 (mod 256)
问题2:tableSizeFor 函数解析
这个函数用于找到大于等于给定容量的最小2的幂。
代码分析
            
            
              arduino
              
              
            
          
           private static final int tableSizeFor(int c) {
     int n = -1 >>> Integer.numberOfLeadingZeros(c - 1);
     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }
        算法步骤
c - 1:处理当 c 本身就是2的幂的情况Integer.numberOfLeadingZeros(c - 1):计算前导零的个数-1 >>> ...:将全1的 -1 右移,得到掩码n + 1:得到最终的2的幂
举例说明
假设 c = 10:
            
            
              ini
              
              
            
          
           c = 10
 c - 1 = 9 (二进制: 00001001)
 前导零个数 = 28 (32位系统中)
 -1 >>> 28 = 00000000000000000000000000001111 (十进制15)
 n + 1 = 16
        结果:16 是大于等于 10 的最小2的幂
更多例子
- c=1 → 1
 - c=5 → 8
 - c=16 → 16
 - c=33 → 64
 
在 ConcurrentHashMap 中的作用
- 保证容量是2的幂 :便于使用位运算 
hash & (n-1)代替取模运算 - 提高性能:位运算比除法运算快得多
 - 优化哈希分布:2的幂容量能更好地分散哈希冲突
 
这个巧妙的位运算技巧来自《Hacker's Delight》这本书,是经典的位操作算法。
源码
看源码还是有很多好处的,就比如说这里。
当然,回过头来,我们可以看一下,好了,现在就不优雅了。
            
            
              ini
              
              
            
          
           @IntrinsicCandidate
 public static int numberOfLeadingZeros(int i) {
     // HD, Count leading 0's
     if (i <= 0)
         return i == 0 ? 32 : 0;
     int n = 31;
     if (i >= 1 << 16) { n -= 16; i >>>= 16; }
     if (i >= 1 <<  8) { n -=  8; i >>>=  8; }
     if (i >= 1 <<  4) { n -=  4; i >>>=  4; }
     if (i >= 1 <<  2) { n -=  2; i >>>=  2; }
     return n - (i >>> 1);
 }
        这个算法实在是太朴素了......
也太强大了!
先看前面有没有16个零,就先右移,右移之后n也-16
之后8个零
4个零
2个零
最后就判断,还有没有1,这里直接减去右移的数就可以了。
但是说起来,还有另外一种更粗暴(当然,也就是优雅)的实现,找到大于等于给定容量的最小2的幂。
            
            
              arduino
              
              
            
          
           /**
  * Returns a power of two table size for the given desired capacity.
  * See Hackers Delight, sec 3.2
  */
 private static final int tableSizeFor(int c) {
     int n = c - 1;
     n |= n >>> 1;
     n |= n >>> 2;
     n |= n >>> 4;
     n |= n >>> 8;
     n |= n >>> 16;
     return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
 }
        我也不懂哪一个更好。前导零的算法应该更通用一些,因为其他地方也用到了这个。
内存操作
            
            
              java
              
              
            
          
           @SuppressWarnings("unchecked")
 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
     return (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
 }
 
 static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                     Node<K,V> c, Node<K,V> v) {
     return U.compareAndSetReference(tab, ((long)i << ASHIFT) + ABASE, c, v);
 }
 
 static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
     U.putReferenceRelease(tab, ((long)i << ASHIFT) + ABASE, v);
 }
        这些方法是 ConcurrentHashMap 中用于直接内存操作的核心方法,通过 Unsafe 类实现高效的并发访问。让我详细解释 ASHIFT 和 ABASE 的计算原理:
方法作用
tabAt():原子性地读取数组元素casTabAt():CAS 原子更新数组元素setTabAt():原子性地设置数组元素
ASHIFT 和 ABASE 的计算
在 ConcurrentHashMap 的静态初始化块中:
            
            
              ini
              
              
            
          
           // 获取 Unsafe 实例
 U = sun.misc.Unsafe.getUnsafe();
 Class<?> ak = Node[].class;
 
 // 计算 ABASE:数组第一个元素的基准地址偏移量
 ABASE = U.arrayBaseOffset(ak);
 
 // 计算 ASHIFT:元素大小对数的移位量
 int scale = U.arrayIndexScale(ak); // 获取数组中每个元素的大小
 if ((scale & (scale - 1)) != 0)    // 检查是否是2的幂
     throw new Error("data type scale not a power of two");
 ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        详细计算过程
1. ABASE (Array Base Offset)
            
            
              ini
              
              
            
          
           ABASE = U.arrayBaseOffset(Node[].class);
        - 作用 :获取 
Node[]数组对象中第一个元素的内存偏移地址 - 值:通常是数组对象头的大小(在64位JVM中约16-24字节)
 
2. ASHIFT (Array Shift)
            
            
              ini
              
              
            
          
           int scale = U.arrayIndexScale(Node[].class);  // 返回每个Node引用的大小
 // 在64位JVM中,开启压缩指针时scale=4,关闭时scale=8
 
 ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
        计算原理:
Integer.numberOfLeadingZeros(scale)计算前导零个数31 - ...得到以2为底的对数
内存地址计算
            
            
              arduino
              
              
            
          
           ((long)i << ASHIFT) + ABASE
        计算过程:
(long)i << ASHIFT=i * scale(通过移位实现乘法)+ ABASE= 加上数组基准偏移量
举例(scale=4, ABASE=16):
- i=0: 
(0 << 2) + 16= 16(第0个元素地址) - i=1: 
(1 << 2) + 16= 20(第1个元素地址) - i=2: 
(2 << 2) + 16= 24(第2个元素地址) 
为什么这样设计?
性能优势
- 避免边界检查:直接内存操作跳过数组边界检查
 - 原子性保证:通过CPU原语实现真正的原子操作
 - 内存屏障控制:精确控制内存可见性
 
内存屏障语义
getReferenceAcquire:获取操作,具有acquire语义putReferenceRelease:设置操作,具有release语义compareAndSetReference:CAS操作,具有完整的原子性
与普通数组访问对比
            
            
              ini
              
              
            
          
           // 普通访问(有边界检查)
 Node<K,V> node = tab[i];
 
 // Unsafe直接访问(无边界检查,有内存屏障)
 Node<K,V> node = (Node<K,V>)U.getReferenceAcquire(tab, ((long)i << ASHIFT) + ABASE);
        这种设计是 ConcurrentHashMap 实现高并发性能的关键技术之一,通过直接内存操作和精确的内存屏障控制,在保证线程安全的同时达到接近原生数组的性能。
spread和哈希
在ConcurrentHashMap的spread方法中,使用了无符号右移操作符>>>而不是有符号右移操作符>>。这是因为>>>和>>在处理负数时的行为不同,而spread方法需要确保生成的哈希值始终是非负的,以避免在哈希表中产生负索引。
有符号右移>>和无符号右移>>>的区别
• 有符号右移>>:
• 有符号右移操作符>>会保留符号位(最高位),即在右移时,左边补符号位。
• 对于正数,>>和>>>的行为相同。
• 对于负数,>>会在左边补 1,结果仍然是负数。
• 无符号右移>>>:
• 无符号右移操作符>>>会忽略符号位,即在右移时,左边补 0。
• 对于正数,>>>的行为与>>相同。
• 对于负数,>>>会在左边补 0,结果变为正数。
spread方法的实现
            
            
              arduino
              
              
            
          
           static final int spread(int h) {
     return (h ^ (h >>> 16)) & HASH_BITS;
 }
        作用
• h ^ (h >>> 16):
• 将哈希值h与其右移 16 位后的值进行异或操作。右移 16 位可以将高 16 位的值移到低 16 位,这样可以更好地混合哈希值的高位和低位,减少哈希冲突。
• 使用>>>确保右移后的值是非负的,即使h是负数,右移后也会变成正数。
• & HASH_BITS:
• 将混合后的哈希值与HASH_BITS进行按位与操作,确保结果在哈希表的有效索引范围内。
• HASH_BITS通常是2^n - 1,用于将哈希值映射到哈希表的索引范围内。
为什么使用>>>而不是>> 假设h是一个负数,例如h = -1(其二进制表示为0xFFFFFFFF):
• 使用>>:
            
            
              ini
              
              
            
          
              int result = (h ^ (h >> 16)) & HASH_BITS;
        • h >> 16的结果是-1(二进制表示为0xFFFFFFFF)。
• h ^ (h >> 16)的结果是0(二进制表示为0x00000000)。
• 0 & HASH_BITS的结果是0。
• 使用>>>:
            
            
              ini
              
              
            
          
              int result = (h ^ (h >>> 16)) & HASH_BITS;
        • h >>> 16的结果是65535(二进制表示为0x0000FFFF)。
• h ^ (h >>> 16)的结果是4294901760(二进制表示为0xFFFF0000)。
• 4294901760 & HASH_BITS的结果是一个非负的哈希值。
总结 使用>>>而不是>>的原因是:
• 确保结果始终是非负的:>>>会将负数右移后变成正数,避免生成负的哈希值。
• 更好的哈希混合:通过将高 16 位的值移到低 16 位,更好地混合哈希值的高位和低位,减少哈希冲突。
因此,使用>>>可以确保spread方法生成的哈希值始终是非负的,并且具有更好的分布特性。
if ((eh = e.hash) == h)
在ConcurrentHashMap的实现中,if ((eh = e.hash) == h)是一个关键的检查步骤,用于快速判断两个节点是否可能相等。这个检查的目的是减少不必要的对象比较操作,提高性能。具体来说,if ((eh = e.hash) == h)检查的是两个节点的哈希值是否相等。如果哈希值不相等,那么这两个节点一定不相等,可以直接跳过后续的比较。
接下来的if ((ek = e.key) == key || (ek != null && key.equals(ek)))是一个更严格的检查,用于确定两个节点的键是否真正相等。这里使用了||(逻辑或)运算符,原因如下:
1.直接引用比较
            
            
              ini
              
              
            
          
           (ek = e.key) == key
        • 作用:首先检查两个键的引用是否相同。
• 原因:如果两个键的引用相同,那么它们一定相等。这种比较非常快速,因为它直接比较了两个对象的内存地址。
• 效率:引用比较非常高效,因为它避免了调用equals方法,减少了不必要的对象比较操作。
2.内容比较
            
            
              csharp
              
              
            
          
           (ek != null && key.equals(ek))
        • 作用:如果两个键的引用不同,但键的值可能相等,需要进一步检查它们的内容是否相等。
• 原因:即使两个键的引用不同,它们的内容也可能相同。例如,两个不同的String对象可能包含相同的字符序列。
• 效率:内容比较相对更耗时,因为它需要调用equals方法,这可能涉及复杂的逻辑,如字符串比较。
为什么使用||运算符
• 短路逻辑:||是一个短路逻辑运算符,即如果第一个条件为true,则不会执行第二个条件。这可以提高效率,因为如果引用比较已经确定两个键相等,就没有必要再进行内容比较。
• 确保准确性:通过结合引用比较和内容比较,可以确保两个键是否真正相等。这在处理哈希表时非常重要,因为哈希表依赖于键的唯一性来存储和检索值。
代码解析
            
            
              ini
              
              
            
          
           if ((eh = e.hash) == h) {
     if ((ek = e.key) == key || (ek != null && key.equals(ek)))
         return e.val;
 }
        • 哈希值比较:
• if ((eh = e.hash) == h):检查当前节点的哈希值e.hash是否与目标哈希值h相等。
• 如果哈希值不相等,直接跳过后续的键比较,因为哈希值不同,键一定不同。
• 键比较:
• if ((ek = e.key) == key):首先检查两个键的引用是否相同。
• 如果引用相同,直接返回当前节点的值e.val,因为键一定相等。
• 如果引用不同,进一步检查内容是否相等:
• ek != null && key.equals(ek):确保ek不为null,然后调用equals方法比较两个键的内容。
• 如果内容相等,返回当前节点的值e.val。
总结 使用||运算符的原因是:
• 提高效率:通过短路逻辑,避免不必要的内容比较。
• 确保准确性:结合引用比较和内容比较,确保两个键是否真正相等。
这种设计在ConcurrentHashMap中非常常见,因为它在保证线程安全的同时,也优化了性能。
eh<0
在ConcurrentHashMap的实现中,else if (eh < 0)这一行代码处理的是特殊情况,即当前节点的哈希值eh是负数。这种情况通常发生在使用了特殊的节点类型,如树化节点(TreeNodes)或转发节点(ForwardingNodes)时。这些节点的哈希值被设置为负数,以区分普通节点。
1.背景知识 在ConcurrentHashMap中,节点的哈希值通常是非负的。但是,为了支持一些特殊功能,如树化(将链表转换为红黑树)和并发控制,某些节点的哈希值被设置为负数。具体来说:
• 树化节点(TreeNodes):当链表长度超过一定阈值时,链表会被转换为红黑树,以提高查找效率。树化节点的哈希值通常设置为负数。
• 转发节点(ForwardingNodes):在并发操作中,某些节点可能被用于转发操作,这些节点的哈希值也设置为负数。
2.代码解析
            
            
              kotlin
              
              
            
          
           else if (eh < 0)
     return (p = e.find(h, key)) != null ? p.val : null;
        2.1 条件检查
            
            
              arduino
              
              
            
          
           else if (eh < 0)
        • eh < 0:检查当前节点的哈希值是否为负数。如果是负数,说明当前节点是一个特殊节点,如树化节点或转发节点。
2.2 调用find方法
            
            
              yaml
              
              
            
          
           (p = e.find(h, key)) != null ? p.val : null;
        • e.find(h, key):调用当前节点的find方法,传入目标哈希值h和键key,尝试找到匹配的节点。
• find方法的具体实现取决于节点的类型。对于树化节点,find方法会在红黑树中查找匹配的节点;对于转发节点,find方法会处理转发逻辑。
• p != null:检查find方法返回的节点是否为null。
• 如果返回的节点不为null,说明找到了匹配的节点,返回该节点的值p.val。
• 如果返回的节点为null,说明没有找到匹配的节点,返回null。
3.具体场景
3.1 树化节点 当链表长度超过一定阈值时,链表会被转换为红黑树。树化节点的哈希值通常设置为负数。在这种情况下,find方法会在红黑树中查找匹配的节点。
3.2 转发节点 在并发操作中,某些节点可能被用于转发操作,这些节点的哈希值也设置为负数。转发节点通常用于处理并发更新操作,如扩容或迁移。
4.为什么需要特殊处理
• 树化节点:红黑树的查找效率比链表高,尤其是在链表长度较长时。通过将链表转换为红黑树,可以显著提高查找效率。
• 转发节点:转发节点用于处理并发操作中的复杂情况,如扩容或迁移。通过使用转发节点,可以确保在并发操作中保持线程安全和高效。
5.总结 这段代码的作用是处理哈希值为负数的特殊节点。具体步骤如下:
• 检查当前节点的哈希值是否为负数。
• 如果是负数,调用find方法在特殊节点中查找匹配的节点。
• 如果找到匹配的节点,返回该节点的值;否则返回null。
这种设计确保了ConcurrentHashMap在处理特殊节点时的高效性和线程安全性。