起源于看到一篇文章HashMap的为啥用尾插法?
HashMap
使用计算对象的hash值来确定在hash表中的存放位置,会遇到hash冲突的情况,即两个不同的对象所计算的hash值在hash表的下标相同。JDK7中使用链表结构解决hash冲突,JDK8使用链表和红黑树(只有冲突的hash链表长度大于8且hash表长度大于64才会转化)解决hash冲突。
虽然相同点都是使用了链表来解决冲突,但是链表插入数据的方法却不同。JDK7使用了头插法,即对象添加都链表的头部;JDK8使用了尾插发,对象添加到链表的尾部。
这种情况下,可以通过遍历HashMap中元素,进行打印对比。HashMap的遍历原理是遍历hash表,如果当前hash表下标的对象不为空,且此对象有后续元素,进行获取处理。根据遍历原理,这两种插入方法的不同,在遍历获取hash表中对象时,会出现两种相反的顺序。
// 例子
public static void main(String[] args) {
// 这个类是JDK7 HashMap源码的拷贝,方便对比
// hash表长度为8,直接就是2的幂,就不会进行长度处理了
// 插入4个元素,小于8*0.75,不会发生扩容,且这些元素的的hash表的下标都是1,会发生hash冲突
HashMap_1_7<Object, Object> map7 = new HashMap_1_7<>(8);
map7.put(1, 1);
map7.put(9, 1);
map7.put(17, 1);
map7.put(25, 1);
System.out.println("jdk7...");
map7.forEach((x, y) -> System.out.println(x));
HashMap<Object, Object> map8 = new HashMap<>(8);
map8.put(1, 1);
map8.put(9, 1);
map8.put(17, 1);
map8.put(25, 1);
System.out.println("jdk8...");
map8.forEach((x, y) -> System.out.println(x));
}
运行结果:
// 顺序可以发现是相反的
jdk7...
25
17
9
1
jdk8...
1
9
17
25
这里就会有个问题,为什么要换成尾插法?
先看下HashMap的扩容原理:
触发:当hash表中存储元素个数大于阈值时(阈值即hash表的长度 * 加载因子,默认0.75)会触发扩容
步骤:
1、创建一个新数组,长度是原来长度的2倍
2、遍历原来的数组,把每一个元素重新计算放入到新数组中,即rehash操作。(所以这一步很消耗性能)
问题就出来了rehash这一步中,在JDK7中,并发下rehash容易会形成环形链表,造成死循环。
例子:
一个hash表,长度是4,key 依次添加为 3 和 11。
这两个元素计算下标值都是3,且在继续添加元素下,hash表长度扩容至8,这时会进行rehash,这两个元素重新计算的下标仍然是3。
结构为 11 —> 3
并发下的rehash:
1、线程1拿到了11,next指向3,这里线程调度结束。
2、线程2进行了rehash,且操作完成。
这时候的结构已经变成 3 —> 11
3、线程1再被调度,这时候继续会出现11后面指向3,拿到3后,3的next又是11。形成了环形,造成死循环。
所以这里JDK8改成尾插法,即使rehash了,前后的链表顺序不变。都是3 —> 11。