ConcurrentHashMap的扩容为什么是2的幂
之所以是2的幂,在于如下的设计:
-
采用key的hashCode与数组长度n减1的逻辑与,将key映射到数组的某个哈希桶中。
hashCode & (n - 1)
中,由于n是2的幂,减1之后可以保证最后几位都是1,只有n的这一位是0。 -
在扩容的时候,根据key的hashCode与n的逻辑与,获取n对应的二进制表示中的1这一位,在hashCode中是1还是0,用于分配该键值对是挂到低位哈希桶中,还是挂到高位哈希桶中。
-
位操作比数学运算速度快,效率高。
如,当n=16的时候,二进制表示:
0000 0000 0000 0000 0000 0000 0001 0000
假设有一个key是“hello”,则它的hashCode是:
String key = "hello";
int hashCode = key.hashCode();
int spreadHash = (hashCode ^ (hashCode >>> 16)) & 0x7fffffff;
System.out.println(spreadHash);
结果是:99163451
变量名称 | 变量值 |
---|---|
n | 0000 0000 0000 0000 0000 0000 0001 0000 |
n - 1 | 0000 0000 0000 0000 0000 0000 0000 1111 |
spreadHash | 0000 0101 1110 1001 0001 1101 0011 1011 |
spreadHash & (n - 1) | 0000 0000 0000 0000 0000 0000 0000 1011 |
在对该spreadHash分配高低哈希桶的时候:
变量名称 | 变量值 |
---|---|
n | 0000 0000 0000 0000 0000 0000 0001 0000 |
spreadHash | 0000 0101 1110 1001 0001 1101 0011 1011 |
spreadHash & n | 0000 0000 0000 0000 0000 0000 0001 0000 |
扩容后的获取:
n = 32 | 0000 0000 0000 0000 0000 0000 0010 0000 |
---|---|
n - 1 | 0000 0000 0000 0000 0000 0000 0001 1111 |
spreadHash | 0000 0101 1110 1001 0001 1101 0011 1011 |
spreadHash & (n - 1) | 0000 0000 0000 0000 0000 0000 0001 1011 |
与其差16的低位的值 | 0000 0000 0000 0000 0000 0000 0000 1011 |
即原来在16中的键值对放到了与原下标相差16的高位哈希桶中,查询的时候在新哈希表中,也可以查询到。
扩容的设计及源码分析
addCount(long x, int check)
参与扩容的第一个线程将sizeCtl设置为扩容戳+2,其他随后的线程在sizeCtl的基础上分别加1。
当扩容线程完成自己负责的哈希桶数组范围元素迁移之后,将sizeCtl设置为sizeCtl - 1,
在哈希桶下标越界的情况下,计算-1之前的值-2是否等于扩展戳,如果等于扩展戳,证明扩容过程结束,处理善后事宜,之后退出扩容过程。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
// 每个n的扩容有唯一的rs值
// 对于n是16的情况,rs的值为:0000 0000 0000 0000 1000 0000 0001 1011 = 32795
int rs = resizeStamp(n);
// 如果sc小于0,表示此时
if (sc < 0) {
if ( // 16
(sc >>> RESIZE_STAMP_SHIFT) != rs // 扩容戳对不上,不参与
||
sc == rs + 1 // 当前扩容是当前线程要参与的上一轮的。
||
sc == rs + MAX_RESIZERS // 如果扩容线程数达到最大值
||
(nt = nextTable) == null // 如果nextTable是null,表示不再扩容,扩容已经结束了
||
transferIndex <= 0 // transferIndex小于等于0,表示已经扩容结束
)
// 无须参与扩容的过程
break;
// 如果需要参与扩容,就将sizeCtl的值设置为sc+1
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
// 扩容
transfer(tab, nt);
}
// 如果sizeCtl的值不小于0,表示还没开始扩容,此时是第一个参加扩容的线程
// 将sizeCtl更新为 rs左移16位,再加上2
// 0000 0000 0000 0000 1000 0000 0001 1011 = 32795
// 1000 0000 0001 1011 0000 0000 0000 0000 = -2145714176 + 2
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
// 扩容
transfer(tab, null);
s = sumCount();
}
}
}
resizeStamp(int n)
扩容戳对每次扩容过程是唯一的,比如16扩容到32,这个过程中不管有多少线程参与,它们的扩容戳是一样的。
sizeCtl的值当发生扩容时,回让扩容戳左移16位,保证时为负值,低16位用于记录参与扩容的线程数以及扩容过程是否结束的状态。
假设n = 16,则二进制表示为:
- 0000 0000 0000 0000 0000 0000 0001 0000
- 1左边有27个0,27的二进制表示:
- 0000 0000 0000 0000 0000 0000 0001 1011
- 逻辑或:
- 0000 0000 0000 0000 1000 0000 0000 0000
- 结果:
- 0000 0000 0000 0000 1000 0000 0001 1011
对于第一个参与扩容的线程,将sizeCtl设置为:
-
首先将扩容戳左移16为:
1000 0000 0001 1011 0000 0000 0000 0000
-
然后再加上2:
1000 0000 0001 1011 0000 0000 0000 0010
对于随后参与扩容的线程,则在此基础上加1:
-
sizeCtl+1:
1000 0000 0001 1011 0000 0000 0000 0011
每个线程执行完自己负责的哈希桶区间的所有哈希桶之后,首先在sizeCtl的基础上 -1
,然后再判断减1之前的值减去2,是否等于扩容戳左移16为的值,如果相等,表示扩容已经完成,该处理善后事宜了。
扩容戳的计算:
/**
* 假设n = 16,则二进制表示为:<br />
* 0000 0000 0000 0000 0000 0000 0001 0000<br />
* 1左边有27个0<br />
* 27的二进制表示:<br />
* 0000 0000 0000 0000 0000 0000 0001 1011<br />
* 逻辑或:<br />
* 0000 0000 0000 0000 1000 0000 0000 0000<br />
* 结果:<br />
* 0000 0000 0000 0000 1000 0000 0001 1011<br />
*
* @param n 哈希桶数组的长度,2的幂
* @return 返回n的迁移戳,n不同,迁移戳不同。同一次扩容过程,迁移戳是相同的。
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
transfer方法
该方法首先计算每个线程应该负责迁移的哈希桶数组区间。
然后对当前线程分配区间,确定第一个迁移的哈希桶数组下标。
当前线程开始迁移元素。
对于链表,通过公式计算每个节点应该分到低位哈希桶还是高位哈希桶。
同时为了提高效率,如果链表的从尾节点向前到达某个节点,它们都应该分配到低位或高位哈希桶,则整体迁移,不再对每个链表元素进行拼接。即,直接用链表尾链直接挂过去。
在链表节点挂低位哈希桶或高位哈希桶的时候,要按照在原来链表中的顺序挂到新的链表中。
对于红黑树,就需要对每个元素进行计算,分配低位哈希桶或高位哈希桶,同时低位或高位哈希桶中依然是红黑树。
不过在迁移的过程中对每个新红黑树计算元素的个数,如果元素个数达不到阈值,就转换回链表。
当前线程完成该哈希桶元素的迁移之后,要计算负责区间的下一个哈希桶,进行元素的迁移。
当该线程完成负责区间的元素都迁移结束后,则尝试再次分配区间,如果分配不到,则线程退出扩容过程。
退出的时候需要:
- 将sizeCtl的值减1
- 判断减1之前减2是否是扩容戳左移16位的值,如果是,则扩容结束;否则仅表示当前线程的工作完成,扩容继续。
/**
* 扩容时键值对节点的迁移
*
* @param tab 现在的哈希桶数组
* @param nextTab 新的哈希桶数组
*/
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
// n是旧数组中哈希桶个数,stride表示每个迁移线程负责的哈希桶个数。
int n = tab.length, stride;
// 根据CPU数量计算每个线程应该负责的哈希桶个数。
// 对于一个线程,从transferIndex开始,到transferIndex - stride这个范围的哈希桶由当前线程负责元素迁移。
// 对于多个线程并发的情况,从更新后的transferIndex-1,到transferIndex - stride范围,由其负责。
// 因此,stride是一个步长,这么多哈希桶,由某个线程负责元素的迁移
// 当然,对于单核心处理器,一个线程负责所有的元素迁移
// 对于多核心处理器,每个线程最少处理MIN_TRANSFER_STRIDE = 16个哈希桶的元素迁移。
// 后续还会有协助迁移元素的线程加进来。此处计算不包括协助迁移线程。
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
// 如果nextTab是null,则创建新数组,新数组的长度是旧数组的两倍
if (nextTab == null) { // initiating
try {
// 创建新数组,并将其赋值给nextTab
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
// 将新数组赋值给nextTable属性
nextTable = nextTab;
// n是新数组中超过旧数组的第一个元素下标
transferIndex = n;
}
// 新数组长度
int nextn = nextTab.length;
// 创建ForwardingNode
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// 该变量表示是否需要查找下一个哈希桶,执行其元素的迁移
// 此处,由于当前线程刚进来,还没干活儿呢,所以将其设置为true
// 如果该变量的值false,则表示查找哈希桶已经有结果了,结果要么是找到了,可以开始干活了
boolean advance = true;
// 如果finishing的值为true,表示扩容结束,可以将tab设置为新的数组了,方法可以返回了
boolean finishing = false;
// 处理哈希桶迁移的循环,每次处理完一个哈希桶元素的迁移,就使用该循环处理下一个哈希桶的元素迁移。
for (int i = 0, bound = 0;;) {
// 用于记录需要进行元素迁移的哈希桶数组元素,该元素要么是链表的头节点,要么是红黑树的TreeBin对象
// fh表示该节点的hash值
Node<K,V> f; int fh;
// while循环用于当前线程查找下一个需要执行元素迁移的哈希桶,即更新i的值,
// advance=true表示需要接着找哈希桶,找到哈希桶之后,将其数组下标赋值给i。
while (advance) {
// nextIndex记录的是找到i之后的值,nextIndex = i + 1
// nextBound记录的是找到i之后的值,nextBound = nextIndex = i + 1
int nextIndex, nextBound;
// 如果i的值不小于bound或者已经完成了,
// finishing为true表示扩容结束,可以使用新数组替换旧数组了
// 则将advance设置为false,当前线程已经完成了它的使命。
// 实际上扩容都结束了,当前线程的advance肯定设置为false。
if (--i >= bound || finishing)
advance = false;
// 如果在新哈希表中的高位索引不大于0,则将advance设置为false,同时i设置为-1
// 如果transferIndex小于等于0,表示当前线程的迁移哈希桶的使命已经完成
// 因为该变量记录的是尚未执行迁移的哈希桶索引最大值+1
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
// 如果条件都合适,则将transferIndex减去stride,或者设置为0,前提是nextIndex要大于stride
// 同时将advance设置为false,表示已经找到当前线程负责处理的哈希桶数组下标了
// transferIndex减去步长,计算当前线程需要迁移的哈希桶范围下限。即这个范围由该线程负责迁移。
// 如果不成功,表示有其他线程抢成功了,当前线程抢下一个范围区间,因为advance还是true。
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
// 实际上nextBound就是nextIndex的值
// 该值满足:i < bound
bound = nextBound;
// nextIndex在这里对于第一次进入的线程,该值还是n
// 如果nextIndex是0,则i小于0
i = nextIndex - 1;
// false表示当前线程的向后退缩查找下一个哈希桶以迁移元素,已经有结果了
advance = false;
}
}
// i记录的是执行迁移的哈希桶在数组中的下标。
// nextn表示新数组的长度。
// 显然,i的值应该在 0 <= i < n 范围;i + n 也肯定小于nextn。
// 如果i的值越界,则可以肯定需要处理扩容如何结束的问题。
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
// 如果迁移结束:
// 1. 将nextTable设置为null,
// 2. 用新数组替代老数组,
// 3. 将sizeCtl设置为下次扩容需要的元素个数
// 方法返回。
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
// 当前线程负责处理的哈希桶数组区间成功完成
// CAS将sizeCtl的值
// 一个线程中途加入扩容,需要将sizeCtl设置为sizeCtl + 1,减去一个线程,则需要:sizeCtl - 1
// 如果sc - 2 的值不是resizeStamp(n) << RESIZE_STAMP_SHIFT,表示扩容还没有结束,当前线程直接返回。
// 如果成立,表示扩容结束,将finishing设置为true。
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
// 加入扩容的第一个线程,将sc设置为resizeStamp(n) << 16 + 2
// 然后每次有一个线程加入,都将sizeCtl+1。
// 由于resizeStamp(n) << RESIZE_STAMP_SHIFT是固定的
// 如果该关系式不成立,表示其他参与扩容的线程还没有结束,仅仅是当前线程的扩容任务完成了
// 它不能将finishing设置为true,因为一旦finishing为true,表示扩容过程结束。
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
// 此时扩容并没有结束,但是当前线程负责的区间已经迁移完成,线程退出。
return;
// 如果当前线程是最后一个扩容的线程,则当前线程结束后,扩容结束。
// 将advance设置为true,表示需要找下一个需要迁移元素的哈希桶
// finishing设置为true表示哈希表已经扩容结束
finishing = advance = true;
// 将i设置为n,下次循环进入该if的外层if,既然finishing已经为true,处理善后适宜,之后扩容结束,线程退出。
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
// 表示i处的hash桶没有元素需要迁移,那只需要通过CAS将其标识为fwd,即表示处理完成。
// 如果成功将该数组元素设置为fwd,则表示当前哈希桶处理完了,应该查找下一个哈希桶执行元素迁移了
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
// 如果该节点的hash值已经是MOVED了,则表示要么已经处理过了,
// 要么其他线程正在执行该哈希桶的元素迁移,因此,当前线程不需要处理了
// 这里将advance设置为true,表示当前线程应该查找下一个需要执行元素迁移的哈希桶了
advance = true; // already processed
// 执行元素的迁移过程
// 如果是红黑树,迁移结束后要判断是否需要退化为链表
else {
// 开始干活了,真正执行元素的迁移了
// 安全起见,先对f节点加锁,别的线程就可以执行其他哈希桶的元素迁移了:分段锁。
// 为什么加锁?由于CHM是分段锁,能够调用扩容方法的线程可能是多个线程并发的。
synchronized (f) {
if (tabAt(tab, i) == f) { // 再次检查数据的一致性
Node<K,V> ln, hn;
// 如果f节点的hash值大于等于0,表示该节点是链表节点
if (fh >= 0) {
// 确定头节点16位的值是1还是0,如果是1,表示该节点应该挂高位哈希桶链表
// 如果是0,表示该节点应该挂低位哈希桶链表
// 该值作为参照,下面的第一个for循环用于查找同属于低位哈希桶链表或低位哈希桶链表的尾链。
int runBit = fh & n;
// 获取f节点
Node<K,V> lastRun = f;
// 从链表头节点开始,查找元素相同的尾链,如果链表中最后几个元素都相同
// 则在迁移的时候,一起移动过去,不用再挨个儿执行链表元素的拼接操作了。
// 尾链头节点用lastRun表示。
// 遍历链表中的各个节点,不断地更新runBit和lastRun的值
// 这两个值表示什么意思?
for (Node<K,V> p = f.next; p != null; p = p.next) {
// 注意:在计算键值对分配到哪个哈希桶的时候,使用的计算公式:hash & (n - 1)。
// 此处的计算使用的是 hash & n。
// 为什么要使用n?
// 以n=16计算,
// 16:0000 0000 0000 0000 0000 0000 0001 0000
// 15:0000 0000 0000 0000 0000 0000 0000 1111
// 32:0000 0000 0000 0000 0000 0000 0010 0000
// 31:0000 0000 0000 0000 0000 0000 0001 1111
// 举例说明:
// 15和31的差值是多少?
// 16:0000 0000 0000 0000 0000 0000 0001 0000
// 15:0000 0000 0000 0000 0000 0000 0000 1111
// 31:0000 0000 0000 0000 0000 0000 0001 1111
// 即对于数组长度是32来讲,如果你的hash值对31逻辑与之后第5位是0
// 效果等同于hash值对31逻辑与操作之后的值。
// 如果与31逻辑与之后的第5位是1,则该结果是hash值对15逻辑与结果加上第5位是1的值,即16
// 可以保证旧数组中i的哈希桶中的各个元素只分配到新数组中i的位置和i+n的位置
// 有利于充分利用分段锁的能力,不会发生线程竞争
// 通过也满足了扩容后这些元素的寻址问题,即rehash的目的。
int b = p.hash & n;
if (b != runBit) {
runBit = b; // 记录应该放到高位哈希桶中还是低位哈希桶中。
lastRun = p; // 链表尾链的第一个节点,后面的都一样,整体迁移即可。
}
}
// runBit如果是0,表示16位是1还是0,如果是0,则(hash & n)的值就是0,一定挂低位哈希桶链表
// 这也是为什么没有判断高位的值,如果16位是1,则runBit就是16,如果是32呢。。。
// 挂高位哈希桶链表的分支直接在else中,优雅。。。
if (runBit == 0) {
ln = lastRun; // 低位是lastRun
hn = null; // 高位节点是null
}
else { // 如果runBit不是0,表示尾链挂高位哈希桶
hn = lastRun; // 此时高位节点设置为lastRun
ln = null; // 低位节点是null
}
// 从链表头节点开始遍历到尾链的第一个节点为止,尾链整体拼接,无须再次遍历。
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
// 如果节点hash值对n逻辑与的值是0,则将该键值对放到新数组的低位哈希桶
// 挂低位链表,倒着挂
// 保证按照原链表顺序将元素挂到新链表中去
// 如果ln是null值,无所谓,因为链表的最后一个元素的next指针一定是null。
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
// 挂高位哈希桶的链表,倒着挂
// 如果hn是null,无所谓,因为链表的最后一个元素的next指针一定是null。
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
// 将低位链表的头节点放到低位哈希桶中
setTabAt(nextTab, i, ln);
// 将高位链表的头节点放到高位哈希桶中
setTabAt(nextTab, i + n, hn);
// 将旧数组中的i处的节点设置为fwd节点,表示当前哈希桶已完成元素迁移
setTabAt(tab, i, fwd);
// 表示当前线程已经迁移完i处的哈希桶元素,需要查找下一个哈希桶,一般是i - 1。
advance = true;
}
// 如果f节点是TreeBin类型,表示此哈希桶中的是红黑树
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
// 低位哈希桶标记
TreeNode<K,V> lo = null, loTail = null;
// 高位哈希桶标记
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
// 遍历红黑树节点
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
// 同链表,放低位哈希桶中
if ((h & n) == 0) {
// 如果loTail是null,则赋值为p节点,此处处理的是第一个键值对
if ((p.prev = loTail) == null)
lo = p;
else // 其他键值对向后追加,组成loTail为首的链表
loTail.next = p;
// 每次循环都更新loTail的值,最后一个节点就是loTail的值
loTail = p;
// 低位哈希桶的键值对个数计数+1
++lc;
}
else {
// 处理第一个节点
if ((p.prev = hiTail) == null)
// p节点作为hi记录
hi = p;
else // 其他的依次向后追加,组成hiTail为首的链表
hiTail.next = p;
// 每次都更新hiTail的值,最后一个节点就是hiTail的值
hiTail = p;
// 高位哈希桶的键值对个数计数+1
++hc;
}
}
// 对低位哈希桶元素计数进行判断,是否需要转换为链表
// 如果红黑树中的元素个数小于等于6,则需要转换回链表,否则直接挂t
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
// 判断高位哈希桶中的元素个数,如果个数小于等于6,则转换回链表,否则直接挂t
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
// 低位哈希桶数组元素设置为ln
setTabAt(nextTab, i, ln);
// 高位哈希桶数组元素设置为hn
setTabAt(nextTab, i + n, hn);
// 将旧数组中的i元素设置为fwd,表示当前哈希桶已完成元素迁移
setTabAt(tab, i, fwd);
// 表示已经进行完当前哈希桶元素迁移,将advance设置为true,
// 表示需要查找下一个需要执行元素迁移的哈希桶了。
advance = true;
}
}
}
}
}
}