【并发】10、HashMap源码分析

面试题+jdk7和jdk8比较

jdk7和jdk8比较

jdk7HashMapjdk8HashMap
数据结构数组+链表数组+链表+红黑树
插入方式头插法尾插法
扩容条件size>threshold且table[i]不为空即当前put进来的元素对应的数组下标位置有值才扩容size>threshold即扩容 或者链表长度大于8且数组长度小于64的时候会扩容
扩容时转移元素的方式一个一个转移通过高低位指针得到两条链再转移
Hash算法更复杂 用很多位运算使生成的hashcode更散列由于增加红黑树,查询性能得到保证,所以没有使用很复杂的hash算法,因为hash函数简单可以减少CPU损耗
是否rehash有可能会重新对key进行哈希(重新Hash跟哈希种子有关系)不会重新计算hash值

为什么jdk8使用红黑树?什么时候将链表转为红黑树?

当元素的个数小于一个阈值,链表的整体插入查询效率要高于红黑树;当元素个数大于阈值时,链表的整体插入查询效率要低于红黑树。因此jdk8引入红黑树,当链表长度大于8且数组长度大于等于64的时候将链表转为红黑树,提高整体的插入查询效率。

jdk7 数组+链表(头插法)

引入链表解决hash冲突,jdk7为头插法

属性

  • HashMap的属性table就是用来存数组
    在这里插入图片描述
  • 加载因子
    数组容量超过了75% 就会去扩容
    为什么加载因子默认选择为75?根据牛顿二项式算的,基于空间与时间的折中考虑。
    因为加载因子过小,也就是过早扩容会浪费很多空间;而加载因子过大,也就是数组都快占满了才扩容的话,这时候发生哈希冲突的概率多,链表就会过长,过长遍历速度就会慢。
    在这里插入图片描述
  • 阈值threshold
    threshold=数组长度加载因子
    默认为16
    0.75=12 即HashMap中存的元素的个数为大于12就会扩容
  • 数组最大长度
    在这里插入图片描述
  • 默认初始容量即数组长度
    默认为16,必须是2的指数幂,如果不是则在初始化的时候会通过roundUpToPowerOf2(size)强行转成比大于等于size的最接近的2的指数幂
    在这里插入图片描述

    为什么数组大小必须是2的指数幂

  1. 通过与运算算index的时候,必须是2的指数幂,减1后进行与运算才能保证index取值范围在0~15之间
  • size hashmap已经添加的元素个数
    在这里插入图片描述
  • modCount 记录操作的次数 put也会加1 remove也会加1
    fast-fail 一种快速容错机制 能及时抛出异常

put()

HashMap1.7中插入一个元素的流程:

  1. 数组为空就创建一个Entry数组
  2. 通过key计算出一个hashcode
  3. 通过hashcode和length-1与操作得到对应的数组下标
  4. 如果当前位置有元素,则遍历这个链表,存在相同的key就替换value值,并返回oldValue
  5. 如果当前数组中元素个数大于阈值,则扩容。即创建一个长度原来2倍的数组,并把数据都转移到新数据中。转移方式就是一个一个转移。
  6. new一个Entry 用头插法插入链表
    在这里插入图片描述
  • 如果数组不存在,则创建inflateTable(threshold)

    在这里插入图片描述

    • roundUpToPowerOf2 将toSize转成最接近的且大于他的二的幂次方数
      二的幂次方数:二进制只有一个位上是1,其余全是0
      因为highestOneBit得到的是小于等于这个数的最大二次幂,所以我们通过number-1再翻倍 传进去 ,就可以得到大于等于number的最小二次幂
      在这里插入图片描述

      highestOneBit 是找到<=这个数的二次幂
      在这里插入图片描述

  • key为null putForNullKey()
    key为null就会将这个entry存到hashmap的第0个位置,如果之前这个位置有entry的key为null,那么value就覆盖
    这个位置不是说只存key为null的,只是key为null的一定是存在这里在这里插入图片描述

  • hash(key) 通过key计算出一个hashcode
    让高位也参与到后续求索引的过程,这样就可以尽量减少哈希冲突
    在这里插入图片描述

  • indexFor(hash, table.length) 通过位运算计算索引
    通过hashcode与“与操作”计算出一个数组下标 注意:这里需要数组长度是2的幂次
    在这里插入图片描述

    为什么是h & (length-1)

    table.length是2的指数次幂,则对应二进制只有一个位是1,其余位置全是0。那这样和别人去做与运算,那只有两种情况,一种是它本身一种是0.
    如果将table.length-1,比如说是15 则就是1111, 这样就可以保证任意一个值和他做与运算后结果都在0到15之间

  • 遍历当前索引上原有的链表
    key相同就替换其value,并返回原有值

  • addEntry()
    在这里插入图片描述

    resize() 扩容

    • resize() 扩容
      当size大于threshold(threshold=数组容量*加载因子)并且当前index位置上有元素时就扩容,扩容为原来数组长度的2倍
      在这里插入图片描述

      • 创建一个新的数组
      • initHashSeedAsNeeded 是否需要生成hash种子

      transfer() 迁移数据

      • 转移数据transfer()
        转移过程就是先遍历数组的每一个位置,如果当前位置有值就遍历链表上的每一个Entry。用头插法移过去,这样和原有顺序就会颠倒。
        rehash是false的话,新的索引只有两种情况:1、原位置2、原位置+oldTable.length在这里插入图片描述
    • createEntry把 key,value封装为一个entry对象,使用头插法插入链表
      在这里插入图片描述
      在这里插入图片描述
      头插法的结果:
      在这里插入图片描述

      jdk7 会采用头插法, 当多线程扩容时(每个线程扩容的时候都会自己new 一个新的数组)会出现循环链表问题,这样就会死锁。原因是:java扩容没有采用任何的同步操作,且为头插法 移到新数组的时候位置会调换,形成环的可能就大大增高。

      在这里插入图片描述

jdk8 数组+链表+红黑树

红黑树

红黑树定义

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 每个叶子节点都是黑色的空节点(NIL节点)。
  4. 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

插入规则

插入的新结点只能是红色
从自己的祖孙三代开始递归执行

  • 父节点是黑色的,不用调整
  • 父节点是红色的
    • 叔叔是空的,则旋转加变色(父节点和祖父结点)
    • 叔叔不是空的
      • 叔叔是红色的,父节点和叔叔都变为黑色,祖父变为红色
      • 叔叔是黑色的,则旋转加变色

时间复杂度

查询和插入都是O(logn)

和ADL、BST对比

BST是二叉查找树,是不平衡的
ADL是完全平衡树,因为要求完全平衡所以插入的时间复杂度高
红黑树是不追求完全平衡的

balanceInsertion() 向红黑树插入结点

每次都是对祖孙三代进行操作,然后递归到root

x:要插入的结点
xp:x的parent
xpp: x的祖父
xppl:x祖父的左孩子
xppr: x祖父的右孩子


在这里插入图片描述

  • x.parent为null,即x是根节点,则将x设置为黑 直接return x
  • x.parent为黑色,或者x的祖父为null,则直接 return root
  • xp是xpp的左孩子 xp == (xppl = xpp.left 为true
    • 叔叔不为空且是红色的,父节点和叔叔都变为黑色,祖父变为红色
    • 叔叔为空或是黑色的,则旋转加变色
      先左旋再右旋
      • x是xp的右孩子,左旋xp rotateLeft(root, x = xp)
      • xp不为null,右旋xpp rotateRight(root, xpp)
  • xp是xpp的右孩子
    区别在于这里的叔叔是xppl,是先右旋再左旋其余逻辑不变
    在这里插入图片描述

rotateLeft() 左旋

rotateLeft(root, x = xp)
第二个参数为要进行选择的结点,即我们要将xp左旋

在这里插入图片描述

  • rl和p互相认亲:r的左孩子rl 作为p的右孩子,rl的父节点即为p
    即把被旋转结点的右孩子的左结点作为被旋转结点的右孩子

  • r认pp为父亲:p的父节点pp作为r的父节点
    即把被旋转结点的右孩子的父节点设置为被旋转结点的父节点
    如果pp为空说明p是根节点,那这步转完后r就是根节点,所以要把r设置为root并变色

  • pp认r为孩子

    • 如果p是pp的左孩子即上图情况,则pp的left就设置为r
      因为r就是代替p,p原来是左孩子,r当然设置为p的左孩子
    • 如果p是pp的右孩子,则pp的right就为r
  • r和p互相认亲

    旋转后结果:

重要属性

  • TREEIFY_THRESHOLD= 8 链表转红黑树的阈值
    当链表长度大于8的时候,会优先看数组的长度是否大于等于64,如果大于64则将链表转为红黑树,否则优先扩容

为什么默认值设为8

通过泊松分布计算一个索引上链表为不同长度的概率,如下图链表长度为8的概率已经很低很低了,所以这个时候转为红黑树?
既然链表长度为8 的概率很低了,那其实遍历8个结点也不很费劲啊,为啥还要转成红黑树呢?
在这里插入图片描述

在这里插入图片描述

  • DEFAULT_INITIAL_CAPACITY = 1 << 4; Hash表默认初始容量
  • MAXIMUM_CAPACITY = 1 << 30; 最大Hash表容量
  • DEFAULT_LOAD_FACTOR = 0.75f;默认加载因子
  • TREEIFY_THRESHOLD = 8;链表转红黑树阈值
  • UNTREEIFY_THRESHOLD = 6;红黑树转链表阈值
  • MIN_TREEIFY_CAPACITY = 64;链表转红黑树时hash表最小容量阈值,达不到优先扩容。

TreeNode<K,V>

转为树的时候会把每个Node 转成TreeNode<K,V>
TreeNode<K,V>为内部类有如下属性:

  • TreeNode<K,V> parent; // red-black tree links 父节点
  • TreeNode<K,V> left; 左孩子
  • TreeNode<K,V> right; 右孩子
  • TreeNode<K,V> prev; // needed to unlink next upon deletion
  • boolean red; 标记元素
  • Node<K,V> next因为是继承的LinkedHashMap,所以也有next属性
    TreeNode既有prev 又有nex 所以是双向链表
    在这里插入图片描述

hash()

得到hash值
不像1.7一样有那么位运算 让他更散列,因为1.8加了红黑树,效率变高了,所以即使散列性没有那么好,插到一个位置影响也没那么大。
在这里插入图片描述

putVal()


HashMap1.8中 插入一个元素的流程:

  1. 数组为空就创建一个Node数组
  2. 通过hashcode和length-1与操作得到对应的数组下标
  3. 下标对应位置没有元素,直接newNode赋给当前位置
  4. 如果当前位置有元素,则有三种情况。
    1. 第一种是当前位置的key和插入元素的key相等;
    2. 第二种是当前位置是红黑树,则向红黑树插入当前结点。
    3. 第三种是链表,则遍历链表向链表的尾部插入元素。如果插入后链表的结点数大于8则且数组长度大于等于64则转成红黑树。
  5. 如果当前数组中元素个数大于阈值,则扩容。即创建一个长度原来2倍的数组,并把数据都转移到新数据中。转移也有三种情况:
    1. 如果当前位置是单个元素,则直接转移
    2. 如果当前位置是红黑树,则先通过高低指针方式将红黑树转成两条链,然后分别看两条链是转成链还是红黑树
      会遍历该位置的双向链表,遍历双向链表统计哪些元素在扩容完之后还是原位置,哪些元素在扩容之后在新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果不小于6,则转成红黑树放到对应的位置,否则转化为单向链表放到对应的位置。
    3. 如果当前位置是链表,则通过高低指针方式转成两条链再移到新的位置。

  • 数组是空的则resize()
  • 通过hash值得到对应的索引
    • 索引处为空,则new Node直接赋值
    • 索引处不为空
      • if 当前key的值和索引处key值相等
      • else if 索引处对象为红黑树
        putTreeVal 插入到红黑树
      • else 即索引处对象为链表
        遍历链表
        • 遍历的时候有key相等的,就修改value。
        • 如果遍历到最后next为空,则new Node,并用尾插法插入。
        • 链表长度大于8(加上要插入的结点共9个),则treeifyBin
          数组为空 或 数组长度小于 64会去resize()扩容, 不会转成红黑树
  • size>threshold 则 resize()扩容
    在这里插入图片描述
    在这里插入图片描述

treeifyBin 树化的准备工作

treeifyBin就是先看是否达到树化条件即数组为空或数组长度小于64的话会去扩容,而不会树化。如果达到树化条件,则做树化前的准备工作:遍历一遍当前索引的链表,把node都转为TreeNode,并且TreeNode是双向链表。然后再进行树化

在这里插入图片描述

  • 数组为空 或 数组长度小于 64 则resize()扩容
  • 根据当前key的hash算出索引,得到当前索引处的对象e
  • 遍历链表,replacementTreeNode把每一个Node结点都转为TreeNode
  • 将TreeNode prev和next都连上对应的TreeNode,即把单链表变为双向链表
  • treeify() 进行树化

treeify() 进行树化

在这里插入图片描述
在这里插入图片描述

  • 遍历TreeNode,判断新结点应该插到哪个位置
    • 比较hash值
      小于左移,大于右移
    • hash值相等,CompareTo比较
      比较 key对应的Class是否实现了Comparable接口,实现了则直接CompareTo()
    • CompareTo比较值相等或没有实现Comparable接口,比较Class名字是否相等
      getClass().getName()
    • Class名字还是相等,则比较identityHashCode
      System.identityHashCode() 这个函数不会调你自己重写的hashCode
  • balanceInsertion 插入结点到红黑树,返回的root就是生成的红黑树的根节点
  • moveRootToFront 把生成的红黑树root放到原来的位置上
    由于root和之前链表的root不一样,而TreeNode是有next和prev 这个函数还通过这两个指针把红黑树的root也作为双向链表的root

resize() 初始化+扩容

这部分包括初始化和扩容
在这里插入图片描述
在这里插入图片描述

  • 如果oldCap为0就初始化
  • oldCap不为0就扩容:遍历每个tab
    • 只有单个对象直接转移
    split() 转移红黑树的元素
    • split() 转移红黑树的元素
      由于红黑树的每个TreeNode都有prev和nex指针,所以这里转移的时候是把它视为双向链表转移

      • 遍历一遍,统计低位的个数lc和高位的个数hc
        在这里插入图片描述

      • loHead不为null
        在这里插入图片描述

        • 低位链的个数小于UNTREEIFY_THRESHOLD默认为6, 则untreeify() 即TreeNode转为Node
        • 否则还是红黑树,直接移到新位置index,若hiHead不为null,说明当前loHead的红黑树的结点要重新树化treeify
      • hiHead不为null
        新位置上index+原数组大小
        在这里插入图片描述

    • 转移链表中的元素(尾插法)
      通过高低位指针的方式 来扩容,遍历链表 将每个节点和原数组容量(默认16)做与运算,这样就只会有两种结果:16或0,我们把与完结果是0的用loHead、loTail去指,与完结果大于0的用hiHead、hiTail去指,这样遍历完这个index上的链表后就有两条链。
      然后我们直接把loHead指的链移到新数组同样索引处,把hiHead指的链移到新数组索引+16处。这样就能保证get的时候还是能从对应索引获取到值。

      在这里插入图片描述

      在这里插入图片描述

      在这里插入图片描述

get()

在这里插入图片描述
在这里插入图片描述

remove()

在这里插入图片描述
removeNode
先找到要删除的元素的位置,然后在看结构是红黑树还是链表进行对应的删除逻辑

  • 找到元素node
    在这里插入图片描述

  • 删除元素node
    在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值