1.ConcurrentHashMap是线程安全的,size()和mappingCount()有什么区别呢?
经查源码,可看到
经比较分析得出:二者返回值不同,为什么会这样?查看英文注释得知,高并发下,如果同时存在insert、remove时,可能会出现不一致,所以推荐使用mappingCount()。
int的范围是 -2^31——2 ^31-1
long的范围是 -2^63——2 ^63-1
2.它在高并发下是怎么保证扩容的?
它的扩容方法是transfer()
代码非常酷,令人赞叹!该方法的执行逻辑如下:
1.通过计算 CPU 核心数和 Map 数组的长度得到每个线程(CPU)要帮助处理多少个桶,并且这里每个线程处理都是平均的。默认每个线程处理 16 个桶。因此,如果长度是 16 的时候,扩容的时候只会有一个线程扩容。
2.初始化临时变量 nextTable。将其在原有基础上扩容两倍。
3.死循环开始转移。多线程并发转移就是在这个死循环中,根据一个 finishing 变量来判断,该变量为 true 表示扩容结束,否则继续扩容。
3.1 进入一个 while 循环,分配数组中一个桶的区间给线程,默认是 16. 从大到小进行分配。当拿到分配值后,进行 i-- 递减。这个 i 就是数组下标。(其中有一个 bound 参数,这个参数指的是该线程此次可以处理的区间的最小下标,超过这个下标,就需要重新领取区间或者结束扩容,还有一个 advance 参数,该参数指的是是否继续递减转移下一个桶,如果为 true,表示可以继续向后推进,反之,说明还没有处理好当前桶,不能推进)
3.2 出 while 循环,进 if 判断,判断扩容是否结束,如果扩容结束,清空临死变量,更新 table 变量,更新库容阈值。如果没完成,但已经无法领取区间(没了),该线程退出该方法,并将 sizeCtl 减一,表示扩容的线程少一个了。如果减完这个数以后,sizeCtl 回归了初始状态,表示没有线程再扩容了,该方法所有的线程扩容结束了。(这里主要是判断扩容任务是否结束,如果结束了就让线程退出该方法,并更新相关变量)。然后检查所有的桶,防止遗漏。
3.3 如果没有完成任务,且 i 对应的槽位是空,尝试 CAS 插入占位符,让 putVal 方法的线程感知。
3.4 如果 i 对应的槽位不是空,且有了占位符,那么该线程跳过这个槽位,处理下一个槽位。
3.5 如果以上都是不是,说明这个槽位有一个实际的值。开始同步处理这个桶。
3.6 到这里,都还没有对桶内数据进行转移,只是计算了下标和处理区间,然后一些完成状态判断。同时,如果对应下标内没有数据或已经被占位了,就跳过了。
4.处理每个桶的行为都是同步的。防止 putVal 的时候向链表插入数据。
4.1 如果这个桶是链表,那么就将这个链表根据 length 取于拆成两份,取于结果是 0 的放在新表的低位,取于结果是 1 放在新表的高位。
4.2 如果这个桶是红黑数,那么也拆成 2 份,方式和链表的方式一样,然后,判断拆分过的树的节点数量,如果数量小于等于 6,改造成链表。反之,继续使用红黑树结构。
4.3 到这里,就完成了一个桶从旧表转移到新表的过程。
好,以上,就是 transfer 方法的总体逻辑。还是挺复杂的。再进行精简,分成 3 步骤:
1.计算每个线程可以处理的桶区间。默认 16.
2.初始化临时变量 nextTable,扩容 2 倍。
3.死循环,计算下标。完成总体判断。
4. 如果桶内有数据,同步转移数据。通常会像链表拆成 2 份。
大体就是上的的 3 个步骤。