ConcurrentHashMap.size() 的实现原理

54 阅读1分钟

1. JDK 1.7 及之前的实现原理

在 JDK 1.7 及之前:

  • ConcurrentHashMap 是 分段锁(Segment Lock)  结构,内部把整个 Map 分成若干个 Segment(默认 16 个),每个 Segment 相当于一个小的 HashTable,各自有锁。

  • size() 实现的时候,会去遍历所有的 Segment,将每个 Segment 的 count(保存了该分段元素个数)累加起来:

    public int size() {
        final Segment<?,?>[] segments = this.segments;
        long sum = 0;
        for (Segment<?,?> seg : segments) {
            if (seg != null) {
                sum += seg.count;
            }
        }
        return (sum > Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int) sum;
    }
    
  • 由于遍历过程中没有把所有 Segment 上锁,所以在计算期间,其他线程可能在插入或者删除元素,导致结果不是精确的。

  • 如果要获得精确结果,JDK7 中 size() 会尝试计算两次,如果前后两次结果一致就直接返回,否则会在第二次遍历时对所有 Segment 加锁保证稳定性(性能较差)

2. JDK 1.8 之后的实现原理

在 JDK 1.8

  • ConcurrentHashMap 去掉了 Segment,采用了类似  LongAdder 的分布式计数思想来减少竞争。

  • 每次插入、删除时都会更新一个全局计数器baseCount 和一组 CounterCell[](与 JDK 的 LongAdder 类似)。

  • size() 计算时,直接累加 baseCount 和 CounterCell 中的值:

    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;
        long sum = baseCount;
        if (as != null) {
            for (CounterCell a : as)
                if (a != null) sum += a.value;
        }
        return sum;
    }
    
  • 好处:完全避免了像 1.7 那样需要锁所有分段来统计,性能更高。

  • 坏处:由于是无锁的累加器结构,统计的值可能在统计过程中被改变,因此 size() 仍然是近似值