Java8:
- 直接返回基础值:若没有竞争,直接返回
baseCount。 - 分散累加:存在竞争时,当多个线程同时更新
baseCount导致 CAS 失败时,会创建CounterCell对象,最后的大小为baseCount和所有CounterCell总和,得到近似总数。
size()方法的最大值是 Integer 类型的最大值,而 Map 的 size() 有可能超过 Integer.MAX_VALUE,所以 JDK8 建议使用 mappingCount(),而不是size(),因为这个方法的返回值是 long 类型,不会因为 size() 方法是 int 类型限制最大值。
详细
ConcurrentHashMap 的 size() 方法在实现时需要在并发环境下平衡性能与准确性。其具体实现机制在 Java 7 和 Java 8 中存在显著差异,以下是分版本解析:
一、Java 7 的实现(基于分段锁)
Java 7 的 ConcurrentHashMap 采用 Segment 分段锁结构,每个 Segment 维护独立的 HashEntry 数组。size() 的实现逻辑如下:
-
两次无锁统计
首次遍历所有 Segment,累加各 Segment 的count(volatile 修饰,保证可见性)和modCount(记录修改次数的变量)。随后进行第二次统计,若两次的modCount总和一致,则说明统计期间没有并发修改,直接返回累加的count总和。 -
加锁重试
如果两次统计的modCount不一致,则对所有 Segment 加锁(阻塞写操作),再次遍历统计所有count,确保准确性。
设计权衡:通过两次无锁统计降低锁竞争概率,仅在检测到并发修改时才加锁,兼顾性能与准确性。
二、Java 8 的实现(基于 CAS + 分段计数)
Java 8 移除了 Segment,采用 数组 + 链表/红黑树 结构,并通过 baseCount 和 CounterCell 数组实现分布式计数:
-
基础计数(baseCount)
无竞争时,直接通过 CAS 操作更新baseCount(volatile 修饰的变量)。 -
竞争处理(CounterCell 数组)
当多个线程同时更新baseCount导致 CAS 失败时,会创建CounterCell对象(类似 LongAdder 的分段计数机制)。每个线程通过哈希分配到不同的CounterCell,减少竞争。 -
最终统计(sumCount())
统计时累加baseCount和所有CounterCell的值。由于CounterCell使用@Contended注解避免伪共享,进一步优化性能。
核心代码逻辑:
final long sumCount() {
CounterCell[] cs = counterCells;
long sum = baseCount;
if (cs != null) {
for (CounterCell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
三、设计优化与对比
| 维度 | Java 7 | Java 8 |
|---|---|---|
| 数据结构 | Segment + HashEntry | 数组 + 链表/红黑树 + CounterCell |
| 锁机制 | 分段锁 | CAS + synchronized(桶级别锁) |
| 计数方式 | 两次无锁统计 + 加锁重试 | 分布式计数(baseCount + CounterCell) |
| 性能瓶颈 | 分段锁粒度较粗 | 更细粒度的锁,并发度更高 |