tableSizeFor 在初始化 hashmap对象时会调用来得到这么一个值,这个值用来作为hashmap 数组大小。
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1; // 无符号 右移 1位,| 符号是 或 操作,一直将后面 的地位全部置1
n |= n >>> 2; // 无符号 右移 2位
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这段代码可以将cap 变成一个2的指数,cap化成二进制后,高位后一位变成1,低位全部变成0,这样得到一个2的指数代表的容量。
拿数字 5 做例子: 5: 0101
for (int i = 0; i < 7; i++) {
System.out.println(tableSizeFor(i));
}
输出结果:
1
1
2
4
4
8
8
大家可以操作一下。
我们来看下表长是7,用于取模运算的时候要减去1,也就是7-1 = 6 ,二进制是 0110 ,左高位,右低位,右边第1位是0,所有数字与它做与运算都是0,不利于均匀散列,而8这个数字,8-1 =7 ,二进制是0111,低位都是1,这样其他数字跟他 与 运算,0&1=0,1&1=1,比较均衡,那为啥减1取模?数组坐标是从0开始的,所以长度为8的数组,坐标范围是0到7。扩容都是2倍扩容,总不能3倍扩容,4倍扩容,太浪费空间了,所以综合初始大小、扩容倍数可以知道表大小一定是2的倍数。同时这么做有几点好处:
这个问题 首先考虑的就是 为什么不是 8 、32 ?
这里的选取主要会考虑:
扩容阈值 = 容量 x 加载因子。
扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量), 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。扩容都是2倍的扩容。
hashmap不是线程安全的,在多线程环境下会出现循环引用情况,我们分析一下,有A、B两个线程,他们都插入数据钱发现要执行扩容,当前情况是有一个数组链表是entry 1,下一个entry 是 entry 2,开始表演:
形成循环引用的原因是表头插入,这样扩容的时候,从表头到表尾取数据,rehash到新的数组坐标下,做表头插入,这样扩容后的顺序是扩容前的逆序,jdk 1.8对此做了优化,每次都是表尾插入,这样顺序跟之前还是一样的。
链表长度达到8就转成红黑树,当长度降到6就转成普通bin,有的资料说因为查找效率,因为:
不过这里有一点,我看了jdk 1.8的链表代码,他是for循环的,for循环的时间复杂度就是 O ( n ) O(n) O(n),不知道为啥在这里就变成 O ( n 2 ) O(\frac{n}{2}) O(2n),估计很多作者在这里理解时用所谓 的平均复杂度来做计算,但实质是平均复杂度的计算没有那么简单,这还跟概率有关,具体可以参考:复杂度分析(下):浅析最好,最坏,平均,均摊时间复、数据结构-最好、最坏、平均时间复杂度的分析(笔记2)
。
所以这个说法不是最终的原因,书中源码这么说:
Because TreeNodes are about twice the size of regular nodes, we
use them only when bins contain enough nodes to warrant use
(see TREEIFY_THRESHOLD). And when they become too small (due to
removal or resizing) they are converted back to plain bins. In
usages with well-distributed user hashCodes, tree bins are
rarely used. Ideally, under random hashCodes, the frequency of
nodes in bins follows a Poisson distribution
(http://en.wikipedia.org/wiki/Poisson_distribution) with a
parameter of about 0.5 on average for the default resizing
threshold of 0.75, although with a large variance because of
resizing granularity. Ignoring variance, the expected
occurrences of list size k are (exp(-0.5)*pow(0.5, k)/factorial(k)).
The first values are:
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
more: less than 1 in ten million
理想情况下使用随机的哈希码,容器中节点分布在hash桶中的频率遵循泊松分布(具体可以查看http://en.wikipedia.org/wiki/Poisson_distribution),按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。
hash算法足够好,也就是碰撞低,从而hash分布遵循泊松分布,那么这样,一个链表中冲突有8个的概率就是0.00000006,非常低,几乎是不太可能的,所以 hash算法不好的话,冲突就非常多,多余8个就为了提高效率换成红黑树。
volatile 修饰,保证了可见性。
1.7使用分段锁,采用了ReentrantLock锁机制来保证,ReentrantLock,一个可重入的互斥锁,它具有与使用synchronized方法和语句所访问的隐式监视器锁相同的一些基本行为和语义,但功能更强大。
分段锁更多的类似二维数组,行表示segment数组,列表示hashEntry数组。segment对象直接继承ReentrantLock。
1.8使用了 CAS + synchronized 来保证并发安全性。
在代码清单“HashEntry 类的定义”中我们可以看到,HashEntry 中的 key,hash,next 都声明为 final 型。这意味着,不能把节点添加到链接的中间和尾部,也不能在链接的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变。这个特性可以大大降低处理链表时的复杂性。
volatile 型变量 count ,特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 , 读线程才需要加锁后重读)。