ConcurrentHashMap size 方法

面试官问:"ConcurrentHashMap 的 size() 方法是怎么实现的?"

候选人小任答不上来。

【面试官心理】 size 方法的实现是 ConcurrentHashMap 的经典设计问题。能说出 LongAdder 分段计数的候选人,说明对并发编程有深入研究。

一、演进历史 🔴

JDK 7:累加 Segment.count

// JDK 7 的 size 方法
public int size() {
    final Segment<K, V>[] segments = this.segments;
    long sum = 0;
    int segcnt = segments.length;

    for (int i = 0; i < segcnt; ++i) {
        Segment<K, V> s = segments[i];
        // 需要锁住 Segment 才能准确计数
        s.lock();
        try {
            sum += s.count;
        } finally {
            s.unlock();
        }
    }

    if (sum > Integer.MAX_VALUE)
        return Integer.MAX_VALUE;
    return (int) sum;
}

// 问题:需要锁住所有 Segment,开销大
// 优化:先不加锁快速估计,如果估计值变化,再加锁精确计数

JDK 8:LongAdder 分段计数

// JDK 8 的 size 方法
public int size() {
    long n = sumCount(); // 调用 sumCount
    return (n < 0L) ? 0 : (n >= (long) Integer.MAX_VALUE) ?
        Integer.MAX_VALUE : (int) n;
}

final long sumCount() {
    CounterCell[] cells = counterCells;
    long sum = baseCount; // baseCount 是基础计数

    if (cells != null) {
        for (CounterCell cell : cells) {
            sum += cell.value; // 累加所有分段
        }
    }
    return sum;
}

二、LongAdder 原理 🟡

// LongAdder:分段计数,减少竞争
// 比 AtomicLong 在高并发下性能更好

// AtomicLong:所有线程竞争一个值
// LongAdder:每个线程有自己的分段,累加时减少竞争

// 内部结构:
public class LongAdder extends Striped64 {
    transient volatile Cell[] cells; // 分段数组
    transient volatile long base;     // 基础值

    public void increment() {
        add(1L);
    }

    public void add(long x) {
        Cell[] cells = this.cells;
        long b, v;
        int m;
        Cell c;

        // CASE 1: cells 未初始化,直接 CAS base
        if ((cells == null || (m = cells.length - 1) < 0 ||
            (c = cells[Thread.currentThread().hashCode & m]) == null) {
            casBase(b = baseCount, b + x); // CAS 更新 base
        }
        // CASE 2: cells 初始化,CAS 更新分段
        else if (casBase(b = baseCount, b + x)) {
            // Success
        }
        // CASE 3: 分段 CAS 失败,扩容分段数组
        else {
            // ...
        }
    }
}

三、size 可能不精确 🟡

// ConcurrentHashMap 的 size() 是不精确的(类似结果)
// 因为:
// 1. put 和 remove 是并发执行的
// 2. size() 在遍历累加的过程中,元素数量可能变化
// 3. 不可能获取"精确"的实时大小(除非锁住整个表)

// JDK 8 的 size():
public int size() {
    long n = sumCount();
    // 如果计数为负(溢出),返回 0
    // 如果计数 >= MAX_VALUE,返回 MAX_VALUE
    return ((n < 0L) ? 0 :
            (n >= (long) Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int) n);
}

// 建议:如果需要精确计数,使用 AtomicLong 自行维护

四、生产建议 🟡

// 场景一:只需要估计大小
// ConcurrentHashMap.size() 是可以的

// 场景二:需要精确计数
AtomicInteger size = new AtomicInteger(0);
map.put(key, value);
size.incrementAndGet();
map.remove(key);
size.decrementAndGet();

// 场景三:需要高性能计数(高并发)
LongAdder count = new LongAdder();
count.increment();
count.sum();

五、追问升级

面试官:"LongAdder 和 AtomicLong 怎么选?"

// 低并发:AtomicLong(简单)
AtomicLong counter = new AtomicLong(0);
counter.incrementAndGet(); // 线程安全

// 高并发:LongAdder(性能更好)
LongAdder counter = new LongAdder();
counter.increment(); // 线程安全,无竞争

// LongAdder 原理:
// 多个线程更新不同的 Cell 分段
// sum() 时累加所有分段
// 比 AtomicLong 的 CAS 重试开销小