ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

面试部分难点梳理 - HashMap + CurrentHashMap

2022-03-21 23:32:26  阅读:163  来源: 互联网

标签:扩容 初始化 HashMap 难点 链表 CurrentHashMap 数组 长度 节点


HashMap

视频
在这里插入图片描述

HashMap的继承体系

在这里插入图片描述

核心属性+构造方法

状态属性:

  • DEFAULT_INITIAL_CAPACITY = 1 << 4; == 16 默认的初始长度
  • MAXIMUN_CAPACITY = 1 << 30; Hash表的最大长度,其是由JVM决定的
  • DEFAULT_LOAD_FACTORY = 0.75f ; 默认的负载因子大小
  • TREEIFY_THRESHOLD = 8; 树化的最小链长
  • UNTREEIFY_THRESHOLD = 6 ;树降级为链的链长
  • MIN_TREEIFY_CAPACITY = 64 ; 链表转化为树时,数组的最小长度。

成员属性

  • Node继承了Map.entry类,
  • Node<K,V>[] table; Hash表的数组
  • size; Hash表的长度
  • modCount; 记录Hash表的修改次数,包括插入、删除等(替换不会计数)。
  • loadFactory ;负载因子
  • threshold ; 扩容阈值(开始时,其数值为tableSizeFor(initCapacity)),但是由于延迟初始化,其并没有真的在内存中分配空间,在后面初始化以后就会变成capacity*loadFactory。
  • 在这里插入图片描述
  • tableSizeFor()函数的目的就是把自定义输入的数值转化成最小的2的n次幂,通过对initCapacity-1 ,不断(对右移1,2,4,8,16位)进行位与操作,得到2的n次幂。
  • 必须是initCapacity,否则在8、16、32等输入2的n次幂的情况下会扩大2倍,变成16、32、64,而不是8、16、32。

putVal()方法

  • putVal一共分为4种情况。

Hash扰动处理

  • 将Hash的高16位与低16位进行异或处理,得到扰动后的Hash数值
  • 扰动后的Hash数值(hash),再将其和(n-1)进行位与操作,得到散列表中的index,在2进制环境下也就是对hash进行取模操作。
  • 在这里插入图片描述

延迟初始化,

  • 如果table为空或者,table的长度为0,这时才初始化数组,以节省内存开销。
    在这里插入图片描述
  • 在这里插入图片描述

第一种情况:Hash数组中没有元素

  • 在这里插入图片描述
  • 找到数组的指定索引位置,插入新节点
  • 这时会跳出判断,++modCount,

第二种情况:Hash数组中有元素并正是我们要找的

  • 在这里插入图片描述
  • 在这里插入图片描述
  • e表示找到了一个我们要找的相同的节点。
  • 更改成为新值,同时返回旧值。

第三种情况:Hash数组中有元素,不是我们要找的找到,并且其数据结构是红黑树

  • 在这里插入图片描述
  • 判断数组中的第一个节点是否是红黑树,如果是,则执行红黑树的插入操作。返回e。
  • 在这里插入图片描述
  • 如果e不为空,则表示原来有值,修改成为新值,返回旧值。

第四种情况:Hash数组中有元素,不是我们要找的找到,并且其数据结构是链表

  • 链表也有三种情况,
    1. 遍历当前链表,找到节点
      在这里插入图片描述
    2. 遍历当前链表,没有找到节点,但是也没有超过树化阈值

    这里用binCount来记录遍历次数,因为遍历是从0开始的,所以当binCount>=TREEIFY_THRESHOLD-1就可以了认为是可以树化了。
    在这里插入图片描述

    1. 遍历当前链表,没有找到节点,达到树化阈值(见上图)

上面的四种情况都会落到这里。

  • 如果e!=null,表明找到了节点,则表示会进行修改,不会++modCount,会直接返回原值。
    在这里插入图片描述
  • 如果e==null,则会++modCount,返回null,表示是插入节点。
    在这里插入图片描述

resize()方法

确定参数

  • oldTab 表示扩容前的数组。
  • oldCap 表示扩容前的长度,如果未被初始化则赋值为0
  • oldThr 表示扩容前的扩容阈值。
  • newCap表示扩容后的长度,newThr表示扩容后的扩容阈值,这一部分主要就是确定这两个数值,让后面根据这两个数值进行扩容处理。
  • 分为4种情况
    在这里插入图片描述

第一种情况:散列表已经初始化过了,正常扩容

  • newCap = oldCap乘以2
  • newCap不会超过MAXIMUM_CAPACITY,同时oldCap大于等于DEFAULT_INITIAL_CAPACITY
  • 会触发正常扩容,
  • 在这种状态下newCap 扩大为oldCap的二倍,newThr 也扩大为oldThr的二倍。
    在这里插入图片描述

第二种情况:散列表没有被初始化过,但是有负载因子(对应HashMap的有参构造)

  • newCap直接赋值成原来的负载因子即可。
  • 其newThr的赋值在第四情况

在这里插入图片描述

第三种情况:散列表没有被初始化过,且没有负载因子(对应HashMap的无参构造)

  • newCap 赋值 DEFAULT_INITIAL_CAPACITY
  • newThr 赋值(int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  • 按照默认参数赋值

在这里插入图片描述

第四种情况:散列表第一次被有参初始化时,或newCap大于MAXIMUM_CAPACITY或oldCap < DEFAULT_INITIAL_CAPACITY,时设定newThr。

  • 还是按照公式ft = (float)newCap * loadFactor;
  • newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
  • 如果超过最大值,则直接赋值Integer.MAX_VALUE,没有则赋值ft,是按照cap*thr得到的结果。
  • 在这里插入图片描述
  • 在这里插入图片描述

扩容操作

  • 在这里插入图片描述
  • 对于原来放到15的二进制可能是01111或者11111,因此01111在扩容后放到15中,11111在扩容后放到31中。当数组中有数据时分为三种情况。
  • 在这里插入图片描述

让e作为节点遍历oldTab[j],同时oldTab[j] == null,辅助GC。

在这里插入图片描述

第一种情况:数组仅仅有一个数据

  • 直接按照hash扰动取余放到新的数组中
  • 在这里插入图片描述

第二种情况:数组有且数据结构为红黑树

  • 调用split方法,拆解红黑树(后面说说红黑树)
  • 在这里插入图片描述

第三种情况:数组有且数据结构为链表

  • 和上面图表达的是一个意思,在上面的图中;
  • 蓝色的放到15的链表为loHead和loTail;
  • 绿色的放到31的链表为hiHead和hiTail;在这里插入图片描述
  • 遍历链表,将高位为0的放到低位链,高位为1的放高位链。
  • 在这里插入图片描述
  • 最后将15的数组的值赋值给低位置链的头,将31的数组的值赋值给高位置链的头。
  • 在这里插入图片描述

get()和remove()

getNode()

  • 在这里插入图片描述
  • 在可以找到的情况下有三种情况

第一种情况:数组中相应索引的第一个元素就是

  • 直接返回。
  • 在这里插入图片描述

第二种情况:数组中响应索引的第一个元素不是,数据结构是红黑树,然后遍历红黑树

  • 在这里插入图片描述

第三种情况:数组中响应索引的第一个元素不是,数据结构是链表,然后遍历链表

  • 在这里插入图片描述

上面三种都没找到,那就没有返回null

removeNode()

  • 这个方法分成两个步骤:查找node和删除node

    在这里插入图片描述

查找

  • 和上面的过程是一样的,判断头节点,判断红黑树,判断链表
  • 这里就不赘述了
    在这里插入图片描述

删除

  • 删除也是分成三种情况
    1. 如果是树节点,则直接调用树的remove函数即可。
    2. 如果是头节点,则将table[index] 赋值给node.next。
    3. 如果是链表则,则p.next = node.next;(p是node的前置节点,node表示找到的节点)(没想通的看上面链表遍历的源代码)。
  • 最后操作modCount++,修改长度等数据。
    在这里插入图片描述

ConcurrentHashMap JDK1.7

  • 视频
  • 对于一个对象的HashCode来说,前几位时用来作为HashEntry的索引使用的,后面的几位时用来作为Segment数组的索引使用的。

构造函数

  • ConcurrentHashMap内部有segment数组,segment数组中存放HashEntry数组,我们的数据是存放到HashEntry中的。
  • 默认值是有segment数组长度是16,HashEntry数组默认长度是2,因此ConcurrentHashMap默认初始化以后的长度为32。
  • 构造方法是没有多线程安全问题的

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
初始时只有第一个segment会初始化两个hashentry,后面的是不会被初始化的,只有在用到才会被初始化

Segment继承了ReentrantLock

  • 一个Segment数组实际上是一个ReentrantLock数组。
  • 不同锁之间是不影响写操作的

put()

  • ConcurrentHashMap的Value不允许有null,key也不允许为null
  • 在这里插入图片描述
  • 如果当前位置没有HashEntry数组,则建立HashEntry数组,按照前面的模板,并计算是否超过阈值,进行扩容。
  • 通过不断的使用UNSAFE。getObjectVolatile()方法取值,来判断当前的位置是否真的没有初始化,如果真的没有,则使用CAS操作,创建HashEntry数组。
    在这里插入图片描述
  • 尝试在需要修改的HashEntry上加锁,然后进行put操作
    在这里插入图片描述
  • 如果该位置为空,则直接加。如果不空,且发生Hash冲突,则拉链法,头插法插入。
  • 对于没拿到锁的对象,则先查询,如果没有则创建一个HashEntry对象,在拿到锁之后就可以直接插入了,来节省时间。创建以后,一直自旋,等待锁。当等到锁以后,使用头插法,来插入。最后的节点插入到hashentry的最前面。如果不需要,则不创建,也不修改,等待锁的释放。

rehash()

  • 详细的看视频吧。
  • 首先rehash()时是对Segment中的HashEntry数组对象进行扩容。因此更加关注HashCode的前几个位。
  • 这里是做了一个优化,如果某一个对象,及其以后所有的对象即使在rehash以后也在同一个数组索引下时,直接将其指针赋值给新的数组的对应位置,这样就可以减少重新计算和插入的次数。
  • 对于在这个之前的对象,CurrentHashMap采用复制节点的方式,以头插法插入对应数组的索引的位置,而不是使用原来的节点。

获取集合长度size()

  • 至少2次(判断last和sum),最多4次(进入锁了)才能返回数组长度。
  • sum将整个CurrentHashMap的modCount求和,来作为标志,判断在获取集合长度的时候是否发生了并发。在求出sum后会将sum赋值给last,用这两个变量判断是否相等,来作为是否发生并发的标志。只有当last == sum的时候,才能表示没有线程对其发生并发操作。
  • size等于所有segment数组中hashEntry数组的个数。
  • 在这里插入图片描述
  • 在这里插入图片描述
  • 在这里插入图片描述

ConcurrentHashMap JDK1.8

  • 扩容那个部分太难了,后面直接看视频吧,这里就记录一些关键点。
  • 视频
  • sizeCtl整个ConcurrentHashMap的核心,其表达的意思:
  • sizeCtl为0,表示数组未初始化,且数组初始化容量为默认值16
  • sizeCtl为正数,如果数组未初始化,那么sizeCtl表示的是数组的初始化容量。如果已经初始化,则表示数组的扩容阈值。
  • sizeCtl为-1,表示数组正在进行初始化。
  • sizeCtl为小于0,表示数组正在扩容,其值为-(n-1)时,表示有n个数组正在扩容,共同完成对数组的扩容。

初始化

  • 默认的loadFactor是 0.75f 是写死的,无法修改。
  • 如果构造函数传入loadFactor 也不会赋值给loadFactory,而是直接进入构造。

默认初始化(不推荐)

  • 设定数组长度为16。没有真的初始化,仅仅是设定初始化函数。
  • 不推荐空参构造 默认16 + 0.75f
  • 在这里插入图片描述

带有初始容量的初始化

  • 建议使用带一个初始初始值容量的初始化方式
  • 同时sizeCtl 赋值为数组长度
  • 在这里插入图片描述
  • 注意这里的tableSizeFor传入的参数不是initialCapacity,而是initialCapacity + (initialCapacity >>> 1) + 1,因此得出的表格长度正常的2倍。
  • 如果initialCapacity = 16,对于HashMap长度为16,如果是CurrentHashMap则为32。
  • 如果initialCapacity = 17,对于HashMap长度为32,如果是CurrentHashMap则为64。

全参初始化

  • 和上面一样,都额外增加了空间
  • 在这里插入图片描述

添加安全

  • 和1.7一样,不允许空值空键。
  • 在这里插入图片描述
  • 在这里插入图片描述

数组初始化(多次自旋进行CAS)

  • 如果数组没有被初始化,或者数组长度为0,则进行数组初始化
  • 在这里插入图片描述
  • 在这里插入图片描述

数值添加(四种情况)

第一种情况:数组当前索引处没有元素

  • 对于元素,不需要上锁,仅通过CAS操作即可
    在这里插入图片描述

第二种情况:数组当前索引处有元素,并且正在进行扩容,则启动多线程辅助扩容。

  • 当某个索引处的链表>8,同时数组长度<64,会进行数组扩容。
  • 将这个索引的插入forward节点,如果再向forward节点,会导致这个节点丢失。
  • 对于forward节点的hash值为MOVED
  • 在这里插入图片描述

对于下面两种情况是上锁的

  • 其上锁的对象是f,也就是数组的第一个元素。
    在这里插入图片描述

第三种情况:数组当前索引处有元素,元素数据结构是链表

  • 前面需要判断一下是否为f,因为如果真的从链表转化成红黑树,f节点就不是了数组对应索引的第一个元素了。
  • 从前到后进行遍历
  • 这里依旧进行树化计数,但是是在后面进行判断是否树化,和树化操作。
  • 在这里插入图片描述
  • 找到了直接返回oldValue
    在这里插入图片描述
  • 如果没找到直接插入,就算已经到了链表长度也要插入
    在这里插入图片描述

第四种情况:数组当前索引处有元素,元素数据结构是红黑树

  • 不是链表结构,从红黑树找元素。
  • 在这里插入图片描述

树化判断

在这里插入图片描述

维护集合长度

在这里插入图片描述

集合长度获取

  • addCount(long x, int check)维护集合长度

  • 这个就不贴代码了,太难了,就放个逻辑在这里吧。

  • 我们首先对baseCount进行CAS操作,操作成功,直接返回,如果操作失败,对CounterCell数组的Value尝试增加,如果还失败进入fullAddCount(x, uncontended);

  • 在fullAddCount(x, uncontended);中,就是不断的自旋的在数组中的人一位置进行加1,如果CAS成功,则返回;如果失败,尝试对数组进行扩容,同时继续尝试随机位置加,直到加成功。CounterCell数组的长度最大为CPU的核心数。
    在这里插入图片描述

  • 每一次如果都CAS失败,则会找一个新的数组位置加,直到加成功
    在这里插入图片描述

  • 则最终的HashMap的总长度为:baseCount + CounterCell_value + CounterCell_value+…+

  • 在这里插入图片描述

扩容安全

  • 依旧是在AddCount()方法内,
  • 第一个扩容的线程,nt 表示新的数组
  • 在这里插入图片描述
  • 协助扩容的进程
  • 在这里插入图片描述
  • 数组迁移是从后面,向前面迁移,每个线程都会领取一定的任务去进行迁移。
  • 多个线程会负责整个数组的数据迁移,每个线程最少负责16个数组数据的迁移
  • advancetrue表示当前线程已经完工,finishingtrue表示所有线程已经完工
  • 新的数组会分配给多个线程一定数量的数据迁移,等到所有的线程的数据迁移都完成了,就表示数据迁移完成了。将新的数组赋值给table。
  • 如果原数组这个位置已经被迁移过了,原数组这个位置会放置一个forwardNode,其hash为MOVED
  • 每个线程的迁移和1.7是一样的,都是采用了取巧的方式
  • 在这里插入图片描述
  • 新的阈值计算: 2n-(n/2) = (3/2)*n = 0.75 * 2n 和 负载因子是0.75的是一样的
  • 在这里插入图片描述

协助扩容的两种情况

  • 插入时插到了hash==MOVED的forwardNode,这时会帮助扩容。
  • 插入成功了,但是在计算集合长度时,发现别的线程在扩容,这时会帮助扩容。

标签:扩容,初始化,HashMap,难点,链表,CurrentHashMap,数组,长度,节点
来源: https://blog.csdn.net/paleatta/article/details/123626434

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有