JDK8和JDK7 HashMap的区别

JDK8的HashMap在链表长度达到8时转换为红黑树以提高查找效率,避免使用AVL因过于平衡导致的插入效率下降。JDK8改用尾插法解决并发扩容时头插法可能导致的环状问题。HashMap容量要求2的指数次幂以确保散列值的均匀分布,初始化通常在第一次put操作时进行。
摘要由CSDN通过智能技术生成

JDK8和JDK7 HashMap的区别

什么是HashMap

Hashmap是一种快速的查找并且插入、删除性能都良好的一种 K/V键值对的数据结构,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。

区别

JDK7JDK8
底层结构数组+链表(hash冲突元素使用链表存储,使用头插法)数组+链表(使用尾插法)+红黑树(当链表长度超过8时,采用红黑树存储)
存储结构使用Entry数组存储数据使用Node数组存储数据
扩容机制执行transfer()方法扩容,先扩容再插入新值。扩容后新的hash值,通过重新hash确定执行reSize()方法扩容,先插入新值在扩容 。扩容后新的hash值,通过更加简单的方式处理,提升效率(通过原hash值 & 原容量 - 1)
线程安全线程不安全(多线程存储数据存在环状问题)线程不安全(多线程存储数据出现数据覆盖问题)
hash算法为了提高遍历效率,所以需要提高hash散列值,所以对hash算法进行复杂化优化哈希算法,因为采用红黑树,大幅度提升了遍历速度,所以为了更好的效率,相对简化hash算法

重点疑问点

  • 为什么在链表长度达到阈值(默认为8)之后,需要转变成红黑树。都是平衡二叉树,为什么使用红黑树,而不是用AVL(平衡二叉树)?

    • 我们都知道,链表在插入的时候速度是最快的,时间复杂度为O(1),但是在读取的时候是O(n),因为需要通过头节点一个个往后遍历,所以当我们的链表长度越来越大的时候,hashMap读取的时间也会越来越大,导致查找的效率变低。而红黑树是一种平衡二叉树,可以自适应的进行长度平衡,使其时间复杂度降低为O(lg n),很大程度上提升了查找效率
    • 至于为什么不使用AVL呢,因为AVL和红黑树虽然都是一种平衡二叉树,他们的时间复杂度都可以达到O(lg n),但是AVL是一种比红黑树更加严格的平衡,所以这也导致了AVL在进行数据插入之后需要进行更多次数的反转之后才能维持AVL的平衡,所以AVL更适合读取查找密集型任务,而红黑树更加适合hashMap这种插入修改密集型任务。
  • 为什么在JDK7链表插入是使用头插法,而到JDK8使用尾插法呢?头插法不是比尾插法效率更高吗?

    • 主要是为了修复并发状态下JDK7扩容时使用头插法产生的环状问题,环状问题产生会导致CPU飙升。
    • 以下为JDK7使用头插法,导致环状问题产生的代码:
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
      threshold = Integer.MAX_VALUE;
      return;
    }
 
    Entry[] newTable = new Entry[newCapacity];
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
  }

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity);
            //采用头插法
            //将e的next设置为newTable[i],也就是i对应的头节点
            //然后将e设置为newTable[i],也就是i对应的头节点
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}
  • 我们通过进一步图解来讲解下,为什么并发状态下扩容时头插法会导致环状问题产生。
    • 首先我们假设当前存在两个线程T1和T2,并且当前数组中存在数据,并且构成链表,线程都判断当前容器需要进行扩容,并且这时候T1优先获得线程执行权,开始执行。
    • T1执行到Entry<K,V> next = e.next之后,也就是将指针指向各个数据,使用一个指针e指向小龙女,那么next就是指向杨过,之后T1线程释放线程执行权,交给T2线程管理在这里插入图片描述
    • T2线程拿到执行权之后,因为当前容器还未扩容完成,所以执行跟T1线程一样的操作,将指针指向各数据,进行扩容,使用头插法,将数据迁移到新的Entry数组中。那么就会得到下面的数据形式,头节点是杨过,杨过的next节点是小龙女。
      在这里插入图片描述
    • 在T2执行完扩容之后,T1又获得线程执行权,这时候就会继续往下之前未执行完的扩容程序。因为指针并不关心具体的值,只关心引用地址。所以指针指向还是没变,e是指向小龙女,e.next指向杨过。但是引用地址相对应的具体值,进行了变动,节点内部属性因为T2原因,导致next属性值进行了变动。所以之后程序的执行,相当于是在T2扩容完成之后的基础上进行的。如下:在这里插入图片描述
      之后执行,那么e指向的小龙女到达新数组的中,next所指向的杨过变成被e指向,如下:
            //将e的next设置为newTable[i],也就是i对应的头节点
            //然后将e设置为newTable[i],也就是i对应的头节点
			e.next = newTable[i];
            newTable[i] = e;
            e = next;

在这里插入图片描述
继续执行后面代码,因为while(null != e) ,所以就会执行Entry<K,V> next = e.next;因为之前T2程序将杨过节点的next属性进行了变更,所以这里next又指向了小龙女,当我们继续往下执行,将杨过节点迁移到新数组中后,再次进行Entry<K,V> next = e.next就会导致环状问题产生:杨过的next属性是小龙女,小龙女的next属性是杨过在这里插入图片描述
所以JDK8使用尾插法是为了解决环状问题,为什么尾插法可以解决环状问题,可以自行验证按照当前JDK7的代码运行步骤,看看是否尾插法会产生环状问题。

  • 为什么HashMap的容量大小一定要是2的指数次幂呢?

    • 因为我们必须保证计算出来的索引大小不能大于容器大小,否则出现数组越界异常,通过与运算的特性就保证了计算出来的散列值,在数组大小范围内。
    • 假如不是2的指数次幂,那么在进行-1操作后再进行与运算时,就会出现计算出来的散列值唯一,导致冲突过多【比如如果是17,那么-1之后就是16,二进制是0001 0000 ,那么无论是什么值与它进行与运算,计算出来的只能是16或者0这两种结果】
    • 可以通过位运算来提升效率。
  • HashMap的初始化是在什么时候?

    • HashMap的初始化容量是在进行put操作的时候
    • 具体的逻辑步骤如下在这里插入图片描述
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值