HashMap遍历顺序在JDK7和JDK8中的不同

起源于看到一篇文章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。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值