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 核心)一样多。顾客随机排队,排太长的队就加开新窗口。

浙公网安备 33010602011771号