JDK1.8中ConcurrentHashMap计算tableSize的细节

发布时间 2023-03-22 21:14:00作者: Torcy

JDK1.8中ConcurrentHashMap关于table的大小和HashMap保持一致,即保证初始容量和每次扩容后的容量都为2的幂,这是为了扩容后很容易计算元素的新位置,即要么是原位置,要么是原位置+oldCapacity,具体细节网上资料很多,这里不多赘述。
学习源码时发现,ConcurrentHashMap计算扩容后tableSize的函数是这样编写的:

/**
 * Returns a power of two table size for the given desired capacity.
 * See Hackers Delight, sec 3.2
 */
private static final int tableSizeFor(int c) {
    int n = c - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

注释中也提到了,这段代码来自于《Hackers Delight》这本书的3.2节,大致去看了一下,代码几乎是完全一致,书中对于这段代码并没有过多的解释,仅提到了一个词对我有所启发,“向右传播”。
这段代码的作用就是,传入一个值c,计算得到另一个值(暂记为d),满足:d大于c且d是尽可能小的2的幂。当然最后对d进行了一些限制,比如不能为负、不能超过MAXIMUM_CAPACITY,这无关紧要。
我们需要知道2的幂有什么特点,即二进制串从左向右看遇到的第一个1是这个串中唯一的1,所以直观地来判断,对于任意的值c,应该如何得到大于c且是2的幂的值d。
假如c的值是00100100010,其中最高位的1的位置记为i,那么值d改如何确定?已知d中只有1个1,且d要尽可能小,那么d中的1必然不能在i的右边,也不能为i,只能为i的左边第1位,即01000000000。
这是直观的判断,代码不好实现,所以书中用了另一种更加巧妙的方式:将位置i及其右边所有位都变成1,然后再将结果加1,就可以得到d。这是因为,根据二进制的计算规则,全为1的串再加1就会变成只有1个1的串。
首先第一行n = c - 1,至于这里为什么要这么做,后面会解释。
我们先来看看n的变化。
建议大家用IDEA调试一下这段代码,然后逐步查看n值的变化,调试时最好以2进制查看n的值。

  • 执行完n = c - 1

  • 执行完n |= n >>> 1

    执行这一行前n的值是:00100000 00000000 00100001 00000000
    n>>>1的值是: 0010000 00000000 00010000 10000000
    n|(n>>>1)的值是: 00110000 00000000 00110001 10000000

  • 执行完n |= n >>> 2

    执行这一行前n的值是:00110000 00000000 00110001 10000000
    n>>>2的值是: 001100 00000000 00001100 01100000
    n|(n>>>2)的值是: 00111100 00000000 00111101 11100000

  • 执行完n |= n >>> 4

    执行这一行前n的值是:00111100 00000000 00111101 11100000
    n>>>4的值是: 001111 00000000 00001111 01111000
    n|(n>>>4)的值是: 00111111 00000000 00111111 11111000

  • 执行完n |= n >>> 8

  • 执行完n |= n >>> 16

至此可以发现,每一行执行前n的值与执行后的值有规律可循,执行后的值是将原来n中的1向右复制了一份。这便是书中提到的“向右传播”。
n|=n>>>1,将原来n中的1向右复制1份,再与原来的n进行或运算,每个1的右边都会出现1个1,这样新的n中的1都是成对出现的。
n|=n>>>2,由于上一步得到的n中的1是成对出现,所以这一次就可以将n中的1向右传播两个位的距离。
n|=n>>>4,将n中的1向右传播4位。
……
假如一开始的n中只有1个1,不论它在哪个位置,右移1位会得到2个1,右移2位会得到4个1,右移4位会得到8个1,右移8位会得到16个1,右移16位会得到32个1,所以经过5次右移后可以保证,无论你传入的是int型的哪一个值,都可以保证:原来n中最高位的1的右边全部都是1。最后只要再加1,就可以保证得到的数字比原来的值大,且是尽可能小的2的幂。