![5aaaa945aaff816b923a9425f6f5b3cb.png](https://img-blog.csdnimg.cn/img_convert/5aaaa945aaff816b923a9425f6f5b3cb.png)
在 Java 基础面试中,HashMap 似乎是每个面试官必问的一道题。
一、阿里开发手册中对 HashMap 的描述
集合初始化时,指定集合初始值大小。
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容 量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能
![577688c55c133976aa9a57994c07f5da.png](https://img-blog.csdnimg.cn/img_convert/577688c55c133976aa9a57994c07f5da.png)
如果你看到这,思考下:
为什么需要放置 1024 个元素,需要 7 次 扩容???
二、HashMap 的数据结构
hashmap 1.7 是 数组 + 单向链表
hashmap 1.8 是 数组 + 单向链表 + 红黑树
以下以 hashmap 1.8 说明
1、 基本属性
![689d70f547484a2dd7eab5e4d9d42ed1.png](https://img-blog.csdnimg.cn/img_convert/689d70f547484a2dd7eab5e4d9d42ed1.png)
特别注意:
除了常说的默认容量 16,负载因子 0.75,由于加入了红黑树,当链表转为红黑树需要满足 2 个必要条件:
① 链表长度到8
![15a629b7dd23ff1f4b6b84ad686690c3.png](https://img-blog.csdnimg.cn/img_convert/15a629b7dd23ff1f4b6b84ad686690c3.png)
② 一个是数组长度到 64
![132395406446aeb40af914ec883b71a0.png](https://img-blog.csdnimg.cn/img_convert/132395406446aeb40af914ec883b71a0.png)
简单说,当你的链表长度即使到 8 ,但是数组长度小于 64 时,赶紧去扩容吧
2、链表结构
链表在Java7 叫 Entry 在 Java8 中叫 Node。
我们从下图中可以看到链表结构是单链表。
红黑树的结构比较复杂,暂时忽略,有兴趣的老铁可自行研究,太伤脑细胞了
![4b9f347d25cc191cd2a4877bcb177483.png](https://img-blog.csdnimg.cn/img_convert/4b9f347d25cc191cd2a4877bcb177483.png)
![29e0010d9f367af687960a8b7126ed9f.png](https://img-blog.csdnimg.cn/img_convert/29e0010d9f367af687960a8b7126ed9f.png)
特别注意:
Java 8 之前是使用头插法(认为后添加的元素会先被查询),在 Java 8 改为尾插法。
由于多线程并发导致的扩容下,头插法由于在并发的时候原来的顺序被另外一个线程 a 颠倒了,而被挂起线程 b 恢复后拿扩容前的节点和顺序继续完成第一次循环后,又遵循 a 线程扩容后的链表顺序重新排列链表中的顺序,最终形成了环。
三、HashMap 如何解决冲突???
在说道 hashmap 如何解决冲突之前,我们先区分下 capacity 和 size 的区别?
1、容量 ( capacity)
比如我们的默认容量是 16,指的是数组的长度,而 size 指的是我们 put 的 key-value 个数
![b611f4abd27fb524c82abfdd2fee79dd.png](https://img-blog.csdnimg.cn/img_convert/b611f4abd27fb524c82abfdd2fee79dd.png)
输出结果:
capacity : 16
size : 1
问题:当我设置默认程度为 10,HashMap 的实际容量是 10 还是 16?
答案:16。在 tableSizeFor 的功能(不考虑大于最大容量的情况)是返回大于输入参数且最近的 2 的整数次幂的数
![bc2155837468ffb053105fe6605a5039.png](https://img-blog.csdnimg.cn/img_convert/bc2155837468ffb053105fe6605a5039.png)
2、hash 函数
![2adccd807a5294b5fc533127afd72787.png](https://img-blog.csdnimg.cn/img_convert/2adccd807a5294b5fc533127afd72787.png)
Java 8 之前 hash 算法 return h & (length-1);
默认容量选择 16 是为了 hash 算法平均分配到数组上
3、扩容
当 size 的个数 达到 (容量*负载因子)的值时,进行扩容。
![ea779fdba6373cebdc7af0784778086b.png](https://img-blog.csdnimg.cn/img_convert/ea779fdba6373cebdc7af0784778086b.png)
四、HashMap 的时间复杂度???
Java 8 HashMap由数组+链表+红黑树组成的,数组是主体,链表是解决冲突,理想情况,不出现 hash 冲突,查找和增加的时间复杂度是 O(1);如果出现 hash 冲突,查找和增加的时间复杂度是 O(n)。
五、HashMap 线程不安全体现在哪些地方?
1、Java 7 中的 HashMap
Java 7 HashMap 链表上的数据是按“头插法”,“头插法”可能导致死循环和数据丢失现象;
![9656ceb4f3c5b5ee9c952dcc4c20d7c2.png](https://img-blog.csdnimg.cn/img_convert/9656ceb4f3c5b5ee9c952dcc4c20d7c2.png)
transfer 函数
① “头插法” 死循环
请花几分钟浏览图中的代码
![fb5942bb109516ac69d87e7ea9fbf274.png](https://img-blog.csdnimg.cn/img_convert/fb5942bb109516ac69d87e7ea9fbf274.png)
假设未扩容前是这个样子
![7d250562f36fc008e33d3690729e8072.png](https://img-blog.csdnimg.cn/img_convert/7d250562f36fc008e33d3690729e8072.png)
假设单线程下,扩容后:
![2a68a53cc934d4a9f0f83e8e2c537c1a.png](https://img-blog.csdnimg.cn/img_convert/2a68a53cc934d4a9f0f83e8e2c537c1a.png)
单线程下没啥问题
假设有两个线程 A 和 B
![f3beaf175a2b2f71e1af3e1e34678dfd.png](https://img-blog.csdnimg.cn/img_convert/f3beaf175a2b2f71e1af3e1e34678dfd.png)
线程 A 出现下列情况
![be0d3b04b05a8e96dfde4d3a4ad00986.png](https://img-blog.csdnimg.cn/img_convert/be0d3b04b05a8e96dfde4d3a4ad00986.png)
线程 A 挂起,线程 B 开始,完成 resize 操作
![2a68a53cc934d4a9f0f83e8e2c537c1a.png](https://img-blog.csdnimg.cn/img_convert/2a68a53cc934d4a9f0f83e8e2c537c1a.png)
到这步,应该不少老铁看出问题了。线程 B k2 的下个节点是 k1,刚才挂起的线程 A 准备 hash k2,由于线程 B 的影响,会导致又回到 重新 hash k1,从而导致死循环。
2、Java 8 中的 HashMap
Java 8 中的 HashMap 使用了“尾插法”,避免了死循环问题。
当 线程 A 和线程 B 都执行到红框处代码时,
如果两条不同数据的 hash 值相同,
并且该位置为 null,
会出现数据覆盖的情况。
![5da85925fc09748a55b629c6f1b4300c.png](https://img-blog.csdnimg.cn/img_convert/5da85925fc09748a55b629c6f1b4300c.png)
@Python大星 | 文