HashMap源码分析

带着问题去剖析源码

  1. 为什么用使用链表+数组的结构?
  2. 用LinkedList代替数组可以吗?
  3. HashMap怎么计算key的位置?
  4. 为什么数组的长度是2的n次方?
  5. 为什么jdk1.8中要用红黑树?
  6. 多线程并发下HashMap会遇到什么问题?循环链表怎么产生的?
  7. 描述一下头插法?
  8. jdk1.7和1.8 HashMap区别

重要变量介绍

  • DEFAULT_INITIAL_CAPACITY Table数组的初始化长度: 1 << 4 2^4=16
  • MAXIMUM_CAPACITY Table数组的最大长度: 1<<30 2^30=1073741824
  • DEFAULT_LOAD_FACTOR 负载因子:默认值为0.75。 当元素的总个数>当前数组的长度 *负载因子。数组会进行扩容,扩容为原来的两倍(todo:为什么是两倍?)
  • TREEIFY_THRESHOLD 链表树化阙值: 默认值为 8。表示在一个node(Table)节点下的值的个数大于8时候,会将链表转换成为红黑树。
  • UNTREEIFY_THRESHOLD 红黑树链化阙值: 默认值为 6 。表示在进行扩容期间,单个Node节点下的红黑树节点的个数小于6时候,会将红黑树转化成为链表。
  • MIN_TREEIFY_CAPACITY = 64 最小树化阈值,当Table所有元素超过改值,才会进行树化(为了防止前期阶段频繁扩容和树化过程冲突)。

HashMap源码剖析

我们都清楚JDK 1.7中HashMap中,采用数组+链表的方式来实现对数据的储存。
在这里插入图片描述

源码中Node体现

 static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

HashMap采⽤Entry数组来存储key-value对,每⼀个键值对组成了⼀个Entry实体,Entry类实际上是⼀个单向的链表结 构,它具有Next指针,可以连接下⼀个Entry实体。 只是在JDK1.8中,链表⻓度⼤于8的时候,链表会转成红⿊树!

为什么使用链表+数组?

要知道为什么使用链表首先需要知道Hash冲突是如何来的,由于我们的数组的值是限制死的,我们在对key值进行散列取到下标以后,放入到数组中时,难免出现两个key值不同,但是却放入到下标相同的格子中,此时我们就可以使用链表来对其进行链式的存放。

我用LinkedList代替数组结构可以吗?

那既然可以使用进行替换处理,为什么有偏偏使用到数组呢? 因为⽤数组效率最⾼!
在HashMap中,定位节点的位置是利⽤元素的key的哈希值对数组⻓度取模得到。此时,我们已得到节点的位置。显然数组的查找效率⽐LinkedList⼤(底层是链表结构)。 那ArrayList,底层也是数组,查找也快啊,为啥不⽤ArrayList?
因为采⽤基本数组结构,扩容机制可以⾃⼰定义,HashMap中数组扩容刚好是2的次幂,在做取模运算的效率⾼。
⽽ArrayList的扩容机制是1.5倍扩容(这一点我相信学习过的都应该清楚),那ArrayList为什么是1.5倍扩容这就不在本⽂说明了。

HashMap怎么计算key的位置?

我们都知道在HashMap中 使用数组加链表,这样问题就来了,数组使用起来是有下标的,但是我们平时使用HashMap都是这样使用的:
在这里插入图片描述
可以看到的是并没有特地为我们存放进来的值指定下标,那是因为我们的hashMap对存放进来的key值进行了hashcode(),生成了一个值,但是这个值很大,我们不可以直接作为下标,此时我们想到了可以使用取余的方法,例如这样:
在这里插入图片描述
即可以得到对于任意的一个key值,进行这样的操作以后,其值都落在0-Table.length-1 中,但是
HashMap的源码却不是这样做? 它对其进行了与操作,对Table的表长度减一再与生产的hash值进行相与:
在这里插入图片描述
我们来画张图进行进一步的了解

在这里插入图片描述
这里我们也就得知为什么Table数组的长度要一直都为2的n次方,只有这样,减一进行相与时候,才能够达到最大的n-1值。 举个栗子来反证一下:
我们现在 数组的长度为 15 减一为 14 ,二进制表示 0000 1110
进行相与时候,最后一位永远是0,这样就可能导致,不能够完完全全的进行Table数组的使用。违背了我们最开始的想要对Table数组进行最大限度的无序使用的原则,因为HashMap为了能够存取高效,,要尽量较少碰撞,就是要尽量把数据分配均匀,每个链表⻓度⼤致相同。
此时还有一点需要注意的是:
我们对key值进行hashcode以后,进行相与时候都是只用到了后四位,前面的很多位都没有能够得到使用,这样也可能会导致我们所生成的下标值不能够完全散列。
解决方案:将生成的hashcode值的高16位于低16位进行异或运算,这样得到的值再进行相与,再和length-1相与一得到最散列的下标值。
在这里插入图片描述
在这里插入图片描述

Put操作

  1. 对key的hashCode()做hash运算,计算index;
  2. 如果没碰撞直接放到bucket⾥;
  3. 如果碰撞了,以链表的形式存在buckets后;
  4. 如果碰撞导致链表过⻓(⼤于等于TREEIFY_THRESHOLD),就把链表转换成红⿊树(JDK1.8中的改动);
  5. 如果节点已经存在就替换old value(保证key的唯⼀性)
  6. 如果bucket满了(超过load factor*currentcapacity),就要resize

在得到下标值以后,可以开始put值进入到数组+链表中,会有三种情况:

  1. 数组的位置为空。
  2. 数组的位置不为空,且面是链表的格式。
  3. 数组的位置不为空,且下面是红黑树的格式。

Get方法

1.对key的hashCode()做hash运算,计算index;
2.如果在bucket⾥的第⼀个节点⾥直接命中,则直接返回;
3.如果有冲突,则通过key.equals(k)去查找对应的Entry;
4. 若为树,则在树中通过key.equals(k)查找,O(logn);
5. 若为链表,则在链表中通过key.equals(k)查找,O(n)。

resise方法

HashMap 的扩容实现机制是将老table数组中所有的Entry取出来,重新对其Hashcode做Hash散列到新的Table中。
在这里插入图片描述

并发resise产生的死锁问题

在这里插入图片描述
出现这种情况的场景是。两个线程同时扩容,B线程阻塞transfer方法,A线程执行完扩展。B线程放行。这时候B线程执行transfer时候,e.next会指回A线程执行完的第二个元素。从而导致循环链表。

Jdk1.8 优化

  1. 由数组+链表的结构改为数组+链表+红⿊树。
  2. 优化了⾼位运算的hash算法:h^(h>>>16)
  3. 扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。

HashMap的并发问题

  1. 多线程扩容,引起的死循环问题
  2. 多线程put的时候可能导致元素丢失
  3. put⾮null元素后get出来的却是null

不安全性的解决方案

  1. 在之前使用hashtable。 在每一个函数前面都加上了synchronized 但是 效率太低 我们现在不常用了。
  2. 使用 ConcurrentHashmap函数,对于这个函数而言 我们可以每几个元素共用一把锁。用于提高效率。

key可以是null吗,value可以是null吗

当然都是可以的,但是对于 key来说只能运行出现一个key值为null,但是可以出现多个value值为null

一般用什么作为key值

⼀般⽤Integer、String这种不可变类当HashMap当key,⽽且String最为常⽤。
(1)因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。
这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。 这就是HashMap中的键往往都使⽤字符串。
(2)因为获取对象的时候要⽤到equals()和hashCode()⽅法,那么键对象正确的重写这两个⽅法是⾮常重要的,这些类已
经很规范的覆写了hashCode()以及equals()⽅法。

jdk1.7和1.8 HashMap区别

  1. 1.8中引入了红黑树,而1.7中没有
  2. 1.8中元素是插在链表的尾部,而1.7中新元素是插在链表的头部
  3. 扩容的时候,1.8中不会出现死循环,而1.7中容易出现死循环,而且链表不会倒置
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值