上篇:《对于HashMap,你知道多少?》

上篇:《对于HashMap,你知道多少?》

 

阅读目录

  • 一、前言
  • 二、源码解读
  • 三、并发场景中使用HashMap会怎么样?
  • 四、怎样合理使用HashMap?

一、前言

HashMap在面试中是个火热的话题,那么你能应付自如吗?下面抛出几个问题看你是否知道,如果知道那么本文对于你来说就不值一提了。

  • HashMap的内部数据结构是什么?
  • HashMap扩容机制时什么?什么时候扩容?
  • HashMap其长度有什么特征?为什么是这样?
  • HashMap为什么线程不安全?并发的场景会出现什么的情况?

本文是基于JDK1.7.0_79版本进行研究的。

二、源码解读

1、类的继承关系

上篇:《对于HashMap,你知道多少?》

 

其中继承了AbstractMap抽象类,别小看了这个抽象类哦,它实现了Map接口的许多重要方法,大大减少了实现此接口的工作量。

2、属性解析

2.1、capacity:容量

  • DEFAULT_INITIAL_CAPACITY:默认的初始容量-必须是2的幂。为什么呢?先留个疑问在这

上篇:《对于HashMap,你知道多少?》

 

  • MAXIMUM_CAPACITY:最大容量为2^30。

2.2 threshold:阈值

上篇:《对于HashMap,你知道多少?》

 

从上面注释可以看出, 它的值是由容量和加载因子决定的。

2.3 loadFactor:加载因子,默认为0.75

上篇:《对于HashMap,你知道多少?》

 

2.4 size:键值对长度

上篇:《对于HashMap,你知道多少?》

 

2.5 modCount:修改内部结构的次数

上篇:《对于HashMap,你知道多少?》

 

上面五个属性字段都很重要, 后面再分析体现其重要。

3、底层数据结构

上篇:《对于HashMap,你知道多少?》

 

Entry内部结构如下:

上篇:《对于HashMap,你知道多少?》

 

经分析后其数据结构为数组+链表的形式,展示图如下:

上篇:《对于HashMap,你知道多少?》

 

4、重要函数

4.1 构造函数

总共有四个构造函数, 主要分析含有两个参数的构造函数:

上篇:《对于HashMap,你知道多少?》

 

其实这个构造函数也主要是初始化加载因子和阈值。(可能1.7的其他版本会有点不一样,会在构造函数中初始化table)

上篇:《对于HashMap,你知道多少?》

 

4.2 put()函数

上篇:《对于HashMap,你知道多少?》

 

  • 第一步:当table还没有初始化时,看下inflateTable()函数做了什么操作。

上篇:《对于HashMap,你知道多少?》

 

  • 其中容量是根据toSize取第一个大于它的2的指数次幂的值, 如下,其中highestOneBit函数是返回其最高位的权值,用的最巧的就是(number - 1) << 1 其实就是取number的倍数, 但综合使用却能取得第一个大于等于该值的2的指数次幂。(用的牛逼)

上篇:《对于HashMap,你知道多少?》

 

  • 接着看put函数的第二步:当key为null时,会取数组下标为0的位置进行链表遍历,如果存在key=null,则替换值并返回。否则进入第六步(注意:索引值依然指定是0)。

上篇:《对于HashMap,你知道多少?》

 

  • 第三步:根据key的hashCode求取hash值,这又是个神奇的算法,这里不做多解释。

上篇:《对于HashMap,你知道多少?》

 

  • 第四步:根据hash值和底层数组的长度计算索引下标。因为数组的长度是2的幂,所以h & (length-1)运算其实就是h与(length-1)的取模运算。不得不服啊,将计算运用的如此高效。

上篇:《对于HashMap,你知道多少?》

 

找个数验证下:

上篇:《对于HashMap,你知道多少?》

 

  • 第五步是验证是否有重复key,如果有则替换新值然后返回,源码很详细了就不再做解释了。
  • 第六步:是将值添加到entry数组中,详细看下addEntry()函数。首先根据size和阈值判断是否需要扩容(进行两倍扩容),如果需要扩容则先扩容重新计算索引,则创建新的元素添加至数组。

上篇:《对于HashMap,你知道多少?》

 

其中扩容机制resize()函数需要重点捞出来晒下:newCapacity = 2 * length,理论上会进行两倍扩容但会根最大容量进行对比取最小, 创建新数组然后将就数组中的值拷贝至新数组(其中会重新计算索引下标),然后再赋值给table, 最后再重新计算阈值。

上篇:《对于HashMap,你知道多少?》

 

接着看transfer()函数,多注意这个函数中循环的内容

上篇:《对于HashMap,你知道多少?》

 

通过上面分析,其实put函数还是简单的,不是很绕。那么能从其中找到开头的第二和第三个问题的答案吗?下面总结下顺便回答下这两个问题:

1、数组长度不管是初始化还是扩容时,都始终保持是2的指数次幂。为什么呢?下面我的分析:

  • 能使元素均匀分布,增大空间利用率。put值时需要根据key的hash值与长度进行取模运算得到索引下标,如果是2的幂,那么length一定是偶数,则length-1一定是奇数,那么它对应的二进制的最后一位一定是1,所以它能保证h&(length-1)既能到奇数也能得到偶数,这样保证了散列的均匀性。相反如果不是2的幂,那么length-1可能是偶数,这样h&(length-1)得到的都是偶数,就会浪费一半的空间了。
  • 运算效率高效。位运算比%运算高效。

2、重复key的值会被新值替换,允许key为空且统一放在下标为0的链表上。

3、当size大于等于阈值(容量*加载因子)时,会进行扩容。扩容机制是:扩容量为原来数组长度的两倍,根据扩容量创建新数组然后进行数组拷贝,新元素落位需要重新计算索引下标。扩容后,阈值需要重新计算,需要插入的元素落位的索引下标也需要重新计算。

4、扩容很耗时,而扩容的次数主要取决于加载因子的值,因为它决定这扩容的次数。下面讲下它的取值的重要性:

  • 加载因子越小,优点:存储的冲突机会减少;缺点:扩容次数越多(消耗性能就越大)、同时浪费空间较大(很多空间还没用,就开始扩容了)
  • 加载因子越大,有点:扩容次数较少,空间利用率高;缺点:冲突几率就变大了、链表(后面介绍)长度会变长,查找的效率降低

5、扩容时会重新计算索引下标。也就是所谓的rehash过程。

6、插入元素都是表头插入,而不是链表尾插入。

4.3、get()函数

知道了put方法的原理,那么get方法就很简单了。

上篇:《对于HashMap,你知道多少?》

 

第一步:如果key为空,则直接从table[0]所对应的链表中查找(应该还记得put的时候为null的key放在哪)。

上篇:《对于HashMap,你知道多少?》

 

第二步:如果key不为空,则根据key获取hash值,然后再根据hash和length-1取模得到索引,然后再遍历索引对应的链表,存在与key相等的则返回。

上篇:《对于HashMap,你知道多少?》

 

三、并发场景中使用HashMap会怎么样?

1、肯定不能保证数据的安全性,因为内部方法没有一个是线程安全的。

2、有时会出现死锁情况。为什么呢?下面列个场景简单分析下:

  • 假设当前容量为4, 有三个元素(a, b, c)都在table[2]下的链表中,另一个元素(d)在table[3]下。如图

上篇:《对于HashMap,你知道多少?》

 

  • 假设此时有A,B两个线程都要往map中put一个元素则都需要扩容,当遍历到table[2]时,假设线程B先进入循环体的第一步:e 指向a, next指向b, 如图:

上篇:《对于HashMap,你知道多少?》

 

上篇:《对于HashMap,你知道多少?》

 

  • 此时线程B让出时间片,让A线程一直执行完扩容操作,最终落位同样也是落位到table[2],其链表元素已经倒序了。如图:

上篇:《对于HashMap,你知道多少?》

 

  • A线程让出时间片,B线程操作:接着循环继续执行,执行到循环末尾的时候,table[2] 指向a, 同时 e 和 next 都是指向b,如图:

上篇:《对于HashMap,你知道多少?》

 

上篇:《对于HashMap,你知道多少?》

 

  • 接着第二轮循环, e = b, next = a, 进行第二轮循环后的结果是e = next 且 table[2] 指向b元素,b元素再指向a元素,如图:

上篇:《对于HashMap,你知道多少?》

 

  • 接着第三轮循环, e = a, a的下个元素为null, 所以next = null,但是当执行到下面这步就改变形式了,e.next 又指向了b,此时a和b已经出现了环形。因为next = null,所以终止了循环。

上篇:《对于HashMap,你知道多少?》

 

上篇:《对于HashMap,你知道多少?》

 

  • 此时,问题还没有直接产生。当调用get()函数查找一个不存在的Key,而这个Key的Hash结果恰好等于3的时候,由于位置3带有环形链表,所以程序将会进入死循环!(上面图形均忽略四个元素和要插入元素的规划)
  • 注:欢迎工作1到5年的Java工程师朋友们加入Java高级交流:468897908。群内提供免费的Java架构学习资料(有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化等...)这些成为架构师必备的知识体系,以及Java进阶学习路线图。

四、怎样合理使用HashMap?

  • 1、创建HashMap时,指定足够大的容量,减少扩容次数。最好为:需要存的实际个数/除以加载因子。可以使用guava包中的Maps.newHashMapWithExpectedSize()方法。

为什么要这样指定大小呢? 再去上面回顾下扩容时机吧

  • 2、不要在并发场景中使用HashMap,如硬要使用通过Collections工具类创建线程安全的map,如:Collections.synchronizedMap(new HashMap<String, Object>());

转载于:https://my.oschina.net/u/3967312/blog/3065994

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值