Skip to main content

concurrenthashmap 中的位运算

· 3 min read
jasper
Docusaurus maintainer
当数组长度 n 是 2 的幂(如 16, 32, 64)时,存在一个数学公式:hash % n == hash & (n - 1)
位运算 & 的执行效率远高于取模运算 %

在哈希表中,我们计算一个 key 的桶位 index,公式是 hash & (n - 1)

假设原来的容量 n = 16(二进制:0001 0000),那么 n - 1 = 15(二进制:0000 1111)。
当容量翻倍变成 2n = 32 时, 2n - 1 = 31(二进制:0001 1111)。
对比新旧掩码的变化:

- 老掩码 (15):0000 1111
- 新掩码 (31):0001 1111

新掩码只比老掩码多了一个二进制位(即第 5 位,也就是 n对应的那个 1)。
因此,一个节点在新数组的位置,完全取决于它的 hash 值在这一高位上是 0 还是 1:
如果这一位是 0:新位置和老位置一模一样(低位)。如果这一位是 1:新位置 = 老位置 + n(高位)。

如果一个 key 的 hash 值是 21,在老容量为 16 的时候,扩容后它就会被分到高位链表,搬去 原索引 + 16 的新位置
21 & 16 = 16

/**
* 生成一个能代表“当前正在进行 n 容量扩容”的唯一stamp
* chm的容量肯定是2^n
* 每一个不同的capacity 前导数(int 32 从左边最高位开始,到第一个1前面有多少个0)肯定不一样
* 就能确定是那一次(16-32)的扩容
* 1 向左移15位 确保合并后的低 16 位结果中,最高位一定不是 0
* n = 16 lz = 27 11011 | 1000 0000 0000 0000 = 1000 0000 0001 1011
* 1 向左移15位 确保合并后的低 16 位结果中,最高位一定不是 0
* 1 负数正在扩容 领先0 原来的容量
*/
static final int resizeStamp(int n) {
return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
}
//============================== helpTransfer 中:
// 在次<< 16位 给整数32位 高16位存储resziestamp 最后结果一定是负数,正在扩容中
// 低16位 参与扩容的线程数
int rs = resizeStamp(tab.length) << RESIZE_STAMP_SHIFT;
// 扩容线程达到上限
// rs+1: 当一个线程发起扩容时 addCount: U.compareAndSetInt(this, SIZECTL, sc, rs + 2)
// +2 : 第一个1 代表正在扩容 第二个1 代表 当前有一个线程进行扩容
// 当sc == rs+1 代表扩容已经完成了
// 扩容任务分配器:64 -48 thread a:[48,63] 减到0就说明已经迁移完成了
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
//如果低位是 0,可能会和某些初始化状态混淆
// 当前线程发起扩容 +1 resize 标记 +1 当前线程
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount();
}
}

//============================== transfer中:
// 扩容完成之后 sziectl = n*2 - n/2 = 0.75n 避免浮点数的运算
sizeCtl = (n << 1) - (n >>> 1);