java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > HashMap数据结构

HashMap底层数据结构详细解析

作者:智由静生

这篇文章主要介绍了HashMap底层数据结构详细解析,HashMap作为开发中常用的数据结构,也是面试中经常被问的知识点,因此作为开发者应该尽可能多的理解其底层的数据结构,需要的朋友可以参考下

一、HashMap的底层数据结构

HashMap作为开发中常用的数据结构,也是面试中经常被问的知识点,因此作为开发者应该尽可能多的理解其底层的数据结构。

创建一个HashMap很简单,假设创建一个人员毕业院校的HashMap

Map<String, String> map = new HashMap<>();
map.put(”张三”: “南京大学”);
map.put(“李四”, “西北工业大学”);

你可能以为数据是这样存储的:

{
        “张三”:  “南京大学”,
        “李四”: ”西北工业大学”
}

但其实它的底层是数组,是这样存储的:

[<”张三”, “南京大学”>, <”李四”,”西北工业大学”>]

但元素并不是顺序放入数组的,它的计算方式是:对key值计算出一个hash值,然后用这个hash值对数组长度取模,根据取模计算结果定位到数组的位置。

假设数组长度是16,对”张三”的hash取模计算结果是4,那么它就放在数组的第5个位置上。实际的存储大约是这样:

[<>, <>, <>, <>, <”张三”, “南京大学”>, <>, <>, <”李四”,”西北工业大学”>, <>, <>, <>, <>, <>, <>, <>, <>]

取出元素的计算过程类似,比如map.get(“张三”),先对”张三”计算出一个hash值,然后用这个hash值对数组长度取模,根据模计算结果定位到数组中的位置,将该位置的元素取出。

二、JDK1.8对HashMap算法的优化

1、对寻址算法的优化

由hash值对数组长度n取模运算,改为hash值与数组长度n减1进行与运算,即hash&(n-1)。这两者在数学上,计算结果是等价的,但从计算机角度来说,后者的运算性能要比前者高很多。

2、对hash算法的优化

不是直接用hashcode值进行运算,而是使用了新的算法,以下是jdk1.8的一段源码:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode() ^ (h >>> 16));
}

hashCode()的返回值是一个32位整数,这个算法的意思就是用hashCode()值右移16位后的值与hashCode()原值进行异或运算。由于右移16位后左侧补0,而与0异或的结果是原值,所以hashCode()的高16位不变,因此此运算相当于hashCode()将的高16位与低16位进行异或运算。

例如:假设有如下一个key值

假设数组长度是16,它的计算过程如下:

那么为什么要进行这样的计算呢?

这是因为,数组长度一般比较小,因此,它的高16位一般都是0。0与任何数进行与运算,结果都是0,因此key值的高16位相当于没起作用,因为结果都一样,实际上就只看低16位的计算结果,这样就增加了计算结果重复的概率。从而增加了hash冲突。改与以上优化算法,让高16位也参与了进来,就能一定程度上减少这种冲突。

三、HashMap如何解决hash碰撞问题

无论hash算法如何优化,对不同的key算出来的hash值是有可能相同的,这种情况叫hash碰撞或者hash冲突。

两个不同的元素不可能放到数组的同一个位置。HashMap的解决方法是,在这个位置放一个链表,链表里可以存多个元素,将相同hash值的元素都存放到这个链表中。当通过get方法读取数据时,当定位到这个位置发现是个链表,就对这个链表进行遍历查询,找到需要的元素。

链表遍历查询的时间复杂度是O(N),当链表比较长时,也就是hash冲突比较多时,性能比较差。因此HashMap对此做了优化,当达到一定条件时,就会将链表转为红黑树。红黑树遍历查询的时间复杂度是O(logN),性能有很大提升。

在JDK1.8之后,HashMap中的链表在同时满足以下两个条件时,将会转化为红黑树(即自平衡的排序二叉树):

1. 条件一:数组 arr[i] 处存放的链表长度大于8;

2. 条件二:数组长度大于64。

满足以上两个条件,数组 arr[i] 处的链表将自动转化为红黑树,其他位置如 arr[i+1] 处的数组元素仍为链表,不受影响。

四、HashMap如何进行扩容

HashMap底层是数组,当数组满了之后,它就会自动进行扩容,变成一个更大的数组,扩容方式就是2倍扩容,数量直接翻倍。由于数组长度变了,而数组长度是参与hash运算的,因此扩容后需要重新进行hash运算,这就可能会产生内容的变化。比如原来有hash冲突需要产生链表,但re-hash运算后没有冲突了,不需要链表了。或者某个位置的链表里有三个元素,进行re-hash运算后,可能变成了两个。

五、ConcurrentHashMap实现线程安全的底层原理

ConcurrentHashMap是线程安全的HashMap,两者都继承自AbstractMap。在需要线程安全的场合操作HashMap需要使用synchronized关键字加锁,性能很低。而ConcurrentHashMap本身就是线程安全,无需再加synchronized关键字,且已经做了优化,可以直接使用。

ConcurrentHashMap的数据结构与HashMap基本相同,底层都是数组。JDK1.7以前采用的是分段加锁,底层不是一个数组,而是分成多个数组。写数据时内部还是加锁的,但是只对所在段的数组加锁,不同段的操作互不影响,所以·可以并行操作,提高了性能。

JDK1.8以后进一步优化和改进,和HashMap一样使用一个大数组的形式。但对某个元素进行put操作时,使用的是CAS操作,这样如果有多个线程操作这个位置的元素,同一时刻只有一个会成功。因此大多数情况下都是无锁操作,性能很高。只有对于有hash冲突而采用链表+红黑树进行处理的位置进行操作时,ConcurrentHashMap内部才需要对这个位置进行synchronized加锁处理。

到此这篇关于HashMap底层数据结构详细解析的文章就介绍到这了,更多相关HashMap数据结构内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文