1. 基本功能与设计目标
ConcurrentHashMap.size()
的核心目标是在并发环境下高效地统计元素总数,同时保证弱一致性(不阻塞其他操作,返回值反映某一时刻的近似值)。
- 非阻塞:不使用全局锁,避免影响其他线程的读写操作。
- 最终一致性:允许返回值与实际值存在短暂偏差,但最终会收敛到真实值。
2. JDK 1.8 的实现机制
JDK 1.8 的 size()
基于 CounterCell
数组 + CAS 操作 实现,核心思想是将计数分散到多个 Cell 中,减少竞争:
-
基础计数变量:
private transient volatile long baseCount; private transient volatile CounterCell[] counterCells;
baseCount
:无竞争时直接累加的基础值。CounterCell[]
:竞争激烈时,将计数分散到不同的 Cell 中。
-
计数流程:
-
无竞争:直接通过 CAS 更新
baseCount
。 -
竞争发生:
- 初始化
CounterCell
数组。 - 通过线程哈希值映射到不同 Cell,使用 CAS 更新对应 Cell 的值。
- 初始化
-
-
CounterCell 结构:
@sun.misc.Contended static final class CounterCell { volatile long value; CounterCell(long x) { value = x; } }
@sun.misc.Contended
注解避免伪共享(False Sharing),提升性能。
伪共享是指多个线程同时读写不同变量,但这些变量恰好位于同一个缓存行(Cache Line)中,导致缓存失效的现象。本质是缓存行层面的资源竞争,虽不直接冲突,但会间接影响性能。
3. size () 方法源码解析
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
long sum = baseCount; // 基础值
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value; // 累加所有Cell的值
}
}
return sum;
}
关键点:
- 求和逻辑:
baseCount
+ 所有CounterCell
的值。 - 弱一致性:遍历过程中其他线程可能修改 Cell 值,导致结果不准确。
4. 并发更新机制
当调用 put()
或 remove()
时,计数更新逻辑如下:
// putVal() 方法中调用
addCount(1L, binCount);
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
// 尝试 CAS 更新 baseCount
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
// CAS 更新失败,尝试更新 CounterCell
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended); // 初始化 CounterCell 或处理冲突
return;
}
if (check <= 1)
return;
s = sumCount();
}
// ... 后续扩容逻辑
}
流程总结:
- 优先 CAS 更新
baseCount
。 - 失败则定位到对应 Cell 并 CAS 更新。
- 若 Cell 为空或仍冲突,调用
fullAddCount()
初始化 Cell 或解决冲突。
5. 与 JDK 1.7 的对比
-
JDK 1.7:
- 基于分段锁(Segment),每个 Segment 维护独立的计数器。
size()
需要遍历所有 Segment 并累加计数,可能需要加锁(若其他线程正在修改)。
-
JDK 1.8:
- 移除分段锁,采用 CAS + CounterCell,性能更优。
- 遍历 Cell 数组无需加锁,弱一致性保证更高吞吐量。
6. 弱一致性的应用场景
size()
的弱一致性适用于:
- 监控统计:如仪表盘显示集合大小,不要求绝对精确。
- 批量操作前置条件:如 “元素超过 1000 时触发清理”,允许少量误差。
7.总结
ConcurrentHashMap 的 size()
方法在 JDK 1.8 中通过 CounterCell 数组 + CAS 操作 实现高效并发计数:
-
分散计数:将元素总数分散存储在
baseCount
和多个CounterCell
中,减少竞争。 -
无锁更新:线程优先 CAS 更新
baseCount
,冲突时按哈希值映射到不同 Cell 进行 CAS 操作。 -
最终求和:
size()
返回baseCount
与所有 Cell 值的累加和,遍历过程中不阻塞其他操作,因此返回值是弱一致性的。
这种设计相比 JDK 1.7 的分段锁机制,避免了全局锁的开销,在高并发场景下性能更优。但需注意,返回值可能不是实时准确的,适用于对统计精度要求不高的场景。”
8.个人思考
-
为什么需要 CounterCell 数组?直接用 AtomicLong 不行吗?
- 高并发下多个线程竞争同一个 AtomicLong 会导致严重的 CAS 冲突,分散到多个 Cell 可减少竞争(类似 LongAdder 的设计)。
-
CounterCell 数组如何处理哈希冲突?
- 通过
ThreadLocalRandom.getProbe()
生成线程哈希值,冲突时通过fullAddCount()
中的advanceProbe()
重新生成哈希值,直到成功。
- 通过
-
sumCount()
遍历 Cell 数组时,如何保证数据可见性?- Cell 数组和元素均被
volatile
修饰,保证线程间的可见性;遍历过程中其他线程的修改可能不被立即感知,体现弱一致性。
- Cell 数组和元素均被
-
在极端情况下(如所有线程都映射到同一个 Cell),性能会怎样?
- 极端情况下会退化为类似 AtomicLong 的性能,但概率极低;
fullAddCount()
中的冲突处理机制会动态调整线程哈希值,避免长期冲突。
- 极端情况下会退化为类似 AtomicLong 的性能,但概率极低;