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()仍然是近似值。