小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
jdk8的ConcurrentHashMap是如何扩容的
transferIndex 表示转移时的下标,初始为扩容前的 length。
ForwardingNode 用于占位。当别的线程发现这个槽位中是 fwd 类型的节点,则跳过这个节点。
sizeCtl 默认为0,用来控制table的初始化和扩容操作。
流程如下:
- 根据操作系统的 CPU 核数和集合 length 计算每个核一轮处理桶的个数,最小是16。之所以可以采用并发扩容是因为数组长度都是2的幂次数,进行选择数组下标存放的时候采用的是(元素hash**&**数组长度-1)这样也便于扩容后元素在新数组中的位置要么就是原位置要么就是原位置+原数组长度。
- 修改 transferIndex 标志位,每个线程领取完任务就减去多少,比如初始大小是transferIndex = table.length = 64,每个线程领取的桶个数是16,第一个线程领取完任务后transferIndex = 48,也就是说第二个线程这时进来是从第 48 个桶开始处理,再减去16,依次类推,这就是多线程协作处理的原理
- 领取完任务之后就开始处理,如果原数组桶位置为空就设置为 ForwardingNode ,如果不为空就加锁拷贝,只有这里用到了 synchronized 关键字来加锁,为了防止拷贝的过程有其他线程在put元素进来。拷贝完成之后也设置为 ForwardingNode节点。
- 如果某个线程分配的桶处理完了之后,再去申请,发现 transferIndex = 0,这个时候就说明所有的桶都领取完了,但是别的线程领取任务之后有没有处理完并不知道,该线程会将 sizeCtl 的值减1表示自己扩容完了,然后判断是不是所有线程都退出了,如果还有线程在处理,就退出。
- 直到最后一个线程处理完,发现 sizeCtl = rs<< RESIZE_STAMP_SHIFT 也就是标识符左移 16 位,才会将旧数组干掉,用新数组覆盖,并且会重新设置 sizeCtl 为新数组的扩容点。
总体上分为2步:
**分配任务:**这部分其实很简单,就是把一个大的数组给切分,切分多个小份,然后每个线程处理其中每一小份,当然可能就只有1个或者几个线程在扩容,那就一轮一轮的处理,一轮处理一份 **处理任务:**复制部分主要有两点,第一点就是加锁,第二点就是处理完之后置为ForwardingNode来占位标识这个位置被迁移过了。