ConcurrentHashMap 是如何进行管理它的容量的,也就是当我们调用它的 size 方法的时候发生了什么?
毕竟是要支持并发,ConcurrentHashMap 的扩容操作比较复杂,我们将从以下几点来带讨论一下它的扩容。
触发扩容
1. 添加新元素后,元素个数达到扩容阈值触发扩容。
2. 调用 putAll 方法,发现容量不足以容纳所有元素时候触发扩容。
3. 某个槽内的链表长度达到 8,但是数组长度小于 64 时候触发扩容。
统计元素个数
触发后面 2 点没啥问题,但是第 1 点中有个小问题,它是如何统计元素的个数呢?它采用和 LongAdder类似的分散热点数据的解决思路,不知道 LongAdder 的小伙伴可以参考往期文章 还在用 AtomicLong?是时候了解一下 LongAdder 了。ConcurrentHashMap 内部定义了一个 CounterCell 的类,它同样被 Contended 修饰防止 volatile 带来的伪共享问题,伪共享不了解可以参考往期文章 面试官问我 volatile 是否存在伪共享问题?我懵逼了。内部实例化了一个 CounterCell 的数组来记录元素的个数,每当线程 put 一个元素到容器中,线程会被映射到一个 CounterCell 的一个元素上面采用 CAS 算法进行加 1 操作,当然如果当前 CounterCell 上已经有线程在操作,或者并发量比较小的话会直接将加 1 累加到 BASECOUNT 上面。
就是依据这样的策略,容器的元素的个数就会游刃有余的计算出来,当需要获取当前容器元素个数的时候,直接将 CounterCell 数据的每个元素值加起来再加上 BASECOUNT 的值就是当前容器中的元素个数。
扩容
上面我们知道了触发扩容的条件为元素个数达到阈值开始扩容,然后也知道了它是如何统计元素的个数的,接下来就看看扩容的运行机制。
首先每个线程承担不小于 16 个槽中的元素的扩容,然后从右向左划分 16 个槽给当前线程去迁移,每当开始迁移一个槽中的元素的时候,线程会锁住当前槽中列表的头元素,假设这时候正好有 get 请求过来会仍旧在旧的列表中访问,如果是插入、修改、删除、合并、compute 等操作时遇到 ForwardingNode,当前线程会加入扩容大军帮忙一起扩容,扩容结束后再做元素的更新操作。
总结
总结一下,对于 ConcurrentHashMap 的扩容我们需要明确如上三点就可以了,扩容触发的时机、元素个数的计算以及具体扩容过程中是如何坚持对外提供服务的就可以了。
如上即为 ConcurrentHashMap 扩容操作的简单介绍,实际 JDK 里面的扩容源码非常复杂。