ConcurrentHashMap中的LongAdder思想
一、设计背景与核心目标
ConcurrentHashMap
在统计元素数量时(如 size()
方法),若直接使用 AtomicLong
会导致以下问题:
- CAS 竞争激烈:高并发下多个线程频繁竞争同一变量,导致大量 CAS 失败和重试,性能急剧下降
- 伪共享(False Sharing):多线程操作同一缓存行的不同变量时,缓存一致性协议会引发无效同步,降低性能
为解决这些问题,ConcurrentHashMap
采用类似 LongAdder
的分段计数机制,将全局计数拆分为多个独立单元,分散线程竞争。
二、分段计数实现原理
1. 核心数据结构
-
baseCount
:基础计数器,用于低并发场景下的原子累加。 -
CounterCell[]
:分段计数器数组,当检测到baseCount
竞争激烈时,动态扩展并分配给不同线程使用
2. 关键方法逻辑
-
addCount(long x, int check)
:- 尝试更新
baseCount
:通过 CAS 操作直接更新基础值。 - 竞争失败时切换分段:若 CAS 失败,初始化
CounterCell
数组,并根据线程哈希值选择特定分段进行更新。 - 动态扩容:若某分段竞争激烈,自动扩容数组并重新哈希,进一步分散冲突
- 尝试更新
-
size()
:最终一致性统计,累加baseCount
和所有CounterCell
的值。由于不阻塞写操作,可能读到中间状态,但多次调用结果一致
3. 伪代码示例
// 伪代码简化版 private void addCount(long x, int check) { if (CAS(baseCount, x)) { // 低竞争直接更新 return; } // 高竞争时使用分段 int hash = ThreadLocalRandom.getProbe(); CounterCell cell = cells[hash & cells.length]; if (cell == null) { // 初始化分段 cells = new CounterCell[INITIAL_CAPACITY]; } CAS(cell.value, x); // 分段内 CAS 更新 }
LongAdder
的核心思想
-
分散热点:
-
传统
AtomicLong
:所有线程通过 CAS 竞争更新同一个value
变量。在高并发下,大量 CAS 失败重试导致性能急剧下降。 -
LongAdder
:引入一个base
变量和一个Cell[]
数组(初始为空或很小)。-
低竞争时: 直接通过 CAS 更新
base
变量(类似AtomicLong
)。 -
高竞争时: 当线程在更新
base
时遇到 CAS 失败(表示有竞争),它会尝试“分散”:-
根据当前线程的某种哈希(如
ThreadLocalRandom
)计算一个索引,定位到Cell[]
数组中的一个Cell
槽位。 -
每个
Cell
是一个独立的、填充过的(避免伪共享)、用volatile
修饰的简单long
值。 -
线程优先尝试通过 CAS 更新自己“命中”的那个
Cell
的值。
-
-
-
核心优势: 将原本针对单个变量的全局竞争,分散到了多个
Cell
变量上。只要线程能够相对均匀地映射到不同的Cell
,竞争就大大减少。冲突只在映射到同一个Cell
的线程间发生,范围大大缩小。
-
-
最终一致求和:
-
要获取
LongAdder
的当前总和 (sum()
),并不是一个简单的读操作。 -
它需要将
base
的值加上Cell[]
数组中所有非空Cell
的值。 -
这个求和过程在并发进行时,读到的
base
和各个Cell
的值可能不是严格意义上同一时刻的快照(即不是原子的),因此sum()
的结果是一个最终一致的近似值。 -
核心优势: 牺牲了读取时绝对的原子性和实时一致性,换取了极高的写入(更新)性能。这对于像统计计数器这样读少写多且对瞬时绝对精确性要求不高的场景是完美的权衡。
-
ConcurrentHashMap
中的借鉴(主要在 addCount
方法)
ConcurrentHashMap
使用 sizeCtl
和一些辅助字段管理容量控制,并使用 baseCount
和一个 CounterCell[]
数组(LongAdder
中的 Cell[]
的变体)来统计元素数量。
-
baseCount
(类比LongAdder.base
):-
一个普通的
volatile long
变量。 -
尝试路径: 当需要增加计数(例如在
putVal
成功插入后调用addCount(1L, binCount)
),ConcurrentHashMap
首先尝试用 CAS 直接更新baseCount
(U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
)。如果 CAS 成功,更新就完成了,非常高效。
-
-
CounterCell[]
(类比LongAdder.cells
):-
一个延迟初始化的数组,元素类型是
CounterCell
(通常就是一个简单的volatile long
包装)。 -
分散路径: 如果步骤 1 中对
baseCount
的 CAS 失败了(表明存在竞争),ConcurrentHashMap
就会进入分散更新模式:-
检查
CounterCell[]
是否已初始化。如果没有,尝试初始化一个较小的大小(通常是 2)。 -
使用线程相关的哈希值(如
ThreadLocalRandom.getProbe()
)计算一个索引i
。 -
尝试获取对应索引
i
处的CounterCell
。-
如果该位置为空,尝试新建一个
CounterCell
并放入数组(需要 CAS 保证线程安全)。 -
如果该位置不为空,尝试用 CAS 更新这个
CounterCell
的值 (U.compareAndSwapLong(c, CELLVALUE, v = c.value, v + x)
)。
-
-
如果在更新选定的
CounterCell
时也遇到 CAS 失败,说明该槽位竞争也激烈,可能会尝试重新计算哈希(ThreadLocalRandom.advanceProbe
)映射到另一个槽位,或者尝试扩容CounterCell[]
数组(如果竞争持续激烈且数组未达上限)。
-
-
核心思想体现: 将对单一
baseCount
的竞争分散到多个CounterCell
上,显著减少线程间的冲突。
-
-
获取元素数量 (
size()
,mappingCount()
):-
类似于
LongAdder.sum()
,ConcurrentHashMap
的size()
或更推荐的mappingCount()
方法并不是简单地返回baseCount
。 -
它们需要遍历整个
CounterCell[]
数组(如果已初始化),将所有非空CounterCell
的值累加到baseCount
上。 -
核心思想体现: 这个求和过程是非原子的。在并发修改时,返回的值是一个估计值,可能略微偏高或偏低(因为可能漏掉某个
CounterCell
刚更新的值,或者包含某个CounterCell
刚被加但尚未合并的值)。这就是为了换取高性能更新而接受的最终一致性。
-
总结:ConcurrentHashMap
中的 LongAdder
思想
-
优先尝试 CAS 更新基础值 (
baseCount
)。 -
遇到基础值更新竞争失败时,将更新操作分散到一组
CounterCell
槽位上。 -
获取总和 (
size()
) 需要遍历求和所有分散的值 (baseCount + ΣCounterCell[i].value
)。 -
读取总和 (
size()
) 的结果是最终一致性的,不是绝对精确的瞬时值,但写入/更新 (addCount
) 的性能在高并发下远优于基于单一原子变量的方案。
这种设计完美契合了 ConcurrentHashMap
的需求:更新计数(put
, remove
)操作极其频繁且要求高性能,而读取计数(size()
)操作相对较少且可以容忍一定的延迟和不精确性。LongAdder
的思想是解决高并发计数器性能问题的经典模式,并被 ConcurrentHashMap
成功采纳用于其内部计数机制。