longadder中countcell数组中数量的变化情况

不是基于线程数量一对一分配的,而是基于"竞争程度"动态扩容的。


一、先纠正一个直觉误区

很多人一听"每个线程在自己对应下标累加",就以为:

线程 1 → cell[0]
线程 2 → cell[1]
线程 3 → cell[2]
...
线程 N → cell[N-1]

错! 如果是这样,那线程一多数组不得爆炸?而且线程频繁创建销毁,数组怎么收缩?

实际情况是:

线程通过哈希"随机映射"到某个 cell,多个线程可能挤在同一个 cell 里。当某个 cell 竞争太激烈时,数组才扩容,把大家分散开。


二、CounterCell 数组是怎么长大的?

阶段 1:没有竞争时,数组都不存在

// 初始状态
counterCells = null;    // 数组都不存在!
baseCount = 0;          // 只有一个基础值

这时候所有线程直接 CAS baseCount,无锁搞定。

阶段 2:出现竞争,数组出生(长度 = 2)

两个线程同时 CAS baseCount,一个成功了,另一个失败。失败的线程发现:

"哟,有人跟我抢了,那我去 counterCells 数组里找个位置"

counterCells = new CounterCell[2];  // 数组诞生,长度只有 2!

线程 A 的哈希值 & 1 = 0 → 去 cell[0]
线程 B 的哈希值 & 1 = 1 → 去 cell[1]

注意:此时数组长度只有 2,不是 16,不是线程数。

阶段 3:还是竞争,继续扩容(翻倍)

又来了线程 C,它的哈希值 & 1 = 0,也要去 cell[0]。但线程 A 正在改 cell[0],C CAS 失败。

"cell[0] 也抢起来了,扩容!"

counterCells = new CounterCell[4];  // 长度翻倍成 4

重新哈希:
线程 A → cell[0] 或 cell[2]
线程 C → cell[0] 或 cell[2]  // 概率分散了

阶段 4:扩到多大算完?

源码里有一个硬限制:

if (n >= NCPU) collide = false;  // 数组长度 >= CPU 核心数,不再扩容

最大长度通常就是 NCPU(CPU 核心数),不是线程数。

为什么?因为同一时刻真正能并行运行的线程数就是 CPU 核心数。数组再长,也只是在浪费内存。


三、线程和 Cell 的关系:不是绑定,是"摇号"

线程池里有 100 个线程,但 counterCells 数组可能只有 4 个:

counterCells = [cell0, cell1, cell2, cell3]

线程 1 (hash=0x1234) & 3 = 0  → 去 cell[0]
线程 2 (hash=0x5678) & 3 = 0  → 也去 cell[0]  ← 撞车了!
线程 3 (hash=0x9ABC) & 3 = 2  → 去 cell[2]
线程 4 (hash=0xDEF0) & 3 = 3  → 去 cell[3]
线程 5 (hash=0x1111) & 3 = 1  → 去 cell[1]
...
线程 99 (hash=0x9999) & 3 = 1 → 也去 cell[1]

看到没?100 个线程共享 4 个 cell,通过哈希"摇号"决定去哪。撞车了就在那排队 CAS,撞得太狠就扩容。


四、核心源码逻辑(简化版)

// 1. 线程的"身份证号"——ThreadLocalRandom 产生的 probe
int h = ThreadLocalRandom.getProbe();

// 2. 如果数组还没初始化,或者长度为 0,走初始化逻辑
if (counterCells == null || counterCells.length == 0) {
    fullAddCount(x, uncontended);  // 初始化数组,长度设为 2
    return;
}

// 3. 用哈希值定位 cell
CounterCell[] cs = counterCells;
int index = (cs.length - 1) & h;   // 和 HashMap 定位 bucket 一样
CounterCell c = cs[index];

// 4. 对这个 cell 做 CAS
if (U.compareAndSwapLong(c, CELLVALUE, v, v + x)) {
    // 成功,完事
} else {
    // 失败,说明这个 cell 竞争激烈
    fullAddCount(x, false);  // 里面可能触发扩容
}

fullAddCount 里的扩容判断:

// 如果数组长度小于 CPU 核心数,就扩容
if (collide || (cs = counterCells) == null || 
    (n = cs.length) >= NCPU) {
    // 不扩容,重新哈希试试
} else {
    // 扩容!长度翻倍
    CounterCell[] rs = new CounterCell[n << 1];
    // 迁移旧数据...
    counterCells = rs;
}

五、一句话总结

CounterCell 数组的长度不是按线程数分配的,而是按"竞争程度"动态扩容的。初始为 null,第一次竞争后变成 2,再激烈就 4、8、16...翻倍增长,但上限是 CPU 核心数。线程通过哈希值"摇号"选择 cell,同一个 cell 可能被多个线程共享,撞车太严重就扩容分散。

类比:

不是给每个顾客发一个收银台(线程数太多会破产),而是先开 2 个收银台,排队人多了就开 4 个,再多开 8 个,最多开到和收银员(CPU 核心)一样多。顾客随机排队,排太长的队就加开新窗口。

posted @ 2026-07-20 15:00  ruo_feng  阅读(9)  评论(0)    收藏  举报
-->