文档处理控制栏:
* [x] 选题收集:2017/12/28
* [ ] 初稿整理:
* [ ] 补充校对:
* [ ] 入库存档:
---
# 图解HashMap(二)
### 概述
上篇分析了HashMap的设计思想以及Java7和Java8源码上的实现,当然还有一些"坑"还没填完,比如大家都知道HashMap是线程不安全的数据结构,多线程情况下HashMap会引起死循环引用,它是怎么产生的?Java8引入了红黑树,那是怎么提高效率的?本篇先填第一个坑,还是以图解的形式加深理解。
### Java7分析
通过上一篇的整体学习,可以知道当存入的键值对超过HashMap的阀值时,HashMap会扩容,即创建一个新的数组,并将原数组里的键值对"转移"到新的数组中。在“转移”的时候,会根据新的数组长度和要转移的键值对key值重新计算在新数组中的位置。重温下Java7中负责"转移"功能的代码
~~~
void transfer(Entry[] newTable, boolean rehash) {
//获取新数组的长度
int newCapacity = newTable.length;
//遍历旧数组中的键值对
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//计算在新表中的索引,并到新数组中
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
~~~
为了加深理解,画个图如下
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eea95cacabc?imageView2/0/w/1280/h/960/ignore-error/1)
这里假设扩容前后5号坑石头、盖伦、蒙多的hash值与新旧数组长度取模运算后还是5。上篇文章也总结了,Java7扩容转移前后链表顺序会倒置。当只有单线程操作hashMap时,一切都是那么美好,但是如果多线程同时操作一个hashMap,问题就来了,下面看下多线程操作一个hashMap在Java7源码下是怎样引起死循环引用。
前戏是这样的:有两个线程分别叫Thread1和Thread2,它们都有操作同一个hashMap的权利,假设hashMap中的键值对是12个,石头和盖伦扩容前后的hash值与新旧数组长度取模运算后还是5。扩容前的模拟堆内存情况如图
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eea91aa7e92?imageView2/0/w/1280/h/960/ignore-error/1)
Thread1得到执行权(Thread2被挂起),Thread1往hashMap里put第13个键值对的时候判断超过阀值,执行扩容操作,Thread1创建了一个新数组,还没来得及转移旧键值对的时候,系统时间片反手切到Thread2(Thread1被挂起),整个过程用图表示
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eea92264bc2?imageView2/0/w/1280/h/960/ignore-error/1)
可以看到Thread1只是创建了个新数组,还没来得及转移就被挂起了,新数组没有内容,此时在Thread1的视角认的是e是石头,next是盖伦;此时的模拟内存图情况
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eea9468e39a?imageView2/0/w/1280/h/960/ignore-error/1)
再看下Thread2的操作,同样Thread2往hashMap里put第13个键值对的时候判断超过阀值,执行扩容操作,Thread2先创建一个新数组,不同的是,Thread2运气好,在时间片轮换前转移工作也走完了。第一次遍历
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eea929d1dd7?imageView2/0/w/1280/h/960/ignore-error/1)
第二次遍历
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eea9397915c?imageView2/0/w/1280/h/960/ignore-error/1)
此时模拟的内存情况
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eead7b0c8b2?imageView2/0/w/1280/h/960/ignore-error/1)
可以看到此时对于盖伦来说,他的next是石头;对于石头来说,它的next为null,隐患就此埋下。接下来时间片又切到Thread1(停了半天终于轮到我出场了),先看下Thread1的处境
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeadbbc896b?imageView2/0/w/1280/h/960/ignore-error/1)
结合代码分析如下
第一步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeade859242?imageView2/0/w/1280/h/960/ignore-error/1)
第二步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeae4dd036d?imageView2/0/w/1280/h/960/ignore-error/1)
第三步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeaec054b6f?imageView2/0/w/1280/h/960/ignore-error/1)
第四步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeadda9a26c?imageView2/0/w/1280/h/960/ignore-error/1)
第五步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb048c6f07?imageView2/0/w/1280/h/960/ignore-error/1)
第六步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb0b1ae0ca?imageView2/0/w/1280/h/960/ignore-error/1)
第七步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb0c31970f?imageView2/0/w/1280/h/960/ignore-error/1)
第八步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb1db16a31?imageView2/0/w/1280/h/960/ignore-error/1)
第九步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb11f5f472?imageView2/0/w/1280/h/960/ignore-error/1)
第10步:
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb31ae99e2?imageView2/0/w/1280/h/960/ignore-error/1)
到这终于看到盖伦和石头"互指",水乳交融。
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb37a1680a?imageView2/0/w/1280/h/960/ignore-error/1)
那这会带来什么后果呢?后续操作新数组的5号坑会进入死循环(注意,操作其他坑并不会有问题),例如Java7 put操作
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb3f375769?imageView2/0/w/1280/h/960/ignore-error/1)
Java7 get操作会执行getEntry,同样会引起死循环。
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb398f2304?imageView2/0/w/1280/h/960/ignore-error/1)
到此,Java7多线程操作HashMap可能形成死循环的原因剖析完成。
### Java8分析
通过上一篇的学习可知,Java7转移前后位置颠倒,而Java8转移键值对前后位置不变。同样的前戏,看下代码
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb3e07d3c9?imageView2/0/w/1280/h/960/ignore-error/1)
此时模拟堆内存情况
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eea9468e39a?imageView2/0/w/1280/h/960/ignore-error/1)
Thread1的情况
![](https://user-gold-cdn.xitu.io/2017/12/4/16021eeb4bfa7149?imageView2/0/w/1280/h/960/ignore-error/1)
这时候Thread2获得执行权,扩容并完成转移工作,通过上篇的学习可知,Java8在转移前会创建两条链表,即扩容后位置不加原数组长度的lo链和要加原数组长度的hi链,这里假设石头和盖伦扩容前后都在5号坑,即这是一条lo链(其实就算不在同一个坑也不影响,原因就是Java8扩容前后链顺序不变)。Thread2遍历第一次![](https://user-gold-cdn.xitu.io/2017/12/3/1601c82a4ca56ccf?imageView2/0/w/1280/h/960/ignore-error/1)
第二次
![](https://user-gold-cdn.xitu.io/2017/12/4/16021efcb47fd57f?imageView2/0/w/1280/h/960/ignore-error/1)
可以看到Thread2全程是没有去修改石头和盖伦的引用关系,石头.next是盖伦,盖伦.next是null。那么Thread1得到执行权后其实只是重复了Thread2的工作。
### 总结
通过源码分析,Java7在多线程操作hashmap时可能引起死循环,原因是扩容转移后前后链表顺序倒置,在转移过程中修改了原来链表中节点的引用关系;Java8在同样的前提下并不会引起死循环,原因是扩容转移后前后链表顺序不变,保持之前节点的引用关系。那是不是意味着Java8就可以把HashMap用在多线程中呢?个人感觉即使不会出现死循环,但是通过源码看到put/get方法都没有加同步锁,多线程情况最容易出现的就是:无法保证上一秒put的值,下一秒get的时候还是原值,建议使用ConcurrentHashMap。
### 感谢
[讲HashMap多线程死循环最详细的外国小哥](https://link.juejin.im/?target=http%3A%2F%2Fjavabypatel.blogspot.ca%2F2016%2F01%2Finfinite-loop-in-hashmap.html)
- 0-发现
- AndroidInterview-Q-A
- Android能让你少走弯路的干货整理
- LearningNotes
- temp
- temp11
- 部分地址
- 0-待办任务
- 待补充列表
- 0-未分类
- AndroidView事件分发与滑动冲突处理
- Spannable
- 事件分发机制详解
- 1-Java
- 1-Java-01基础
- 未归档
- 你应该知道的JDK知识
- 集合框架
- 1-Java-04合集
- Java之旅0
- Java之旅
- JAVA之旅01
- JAVA之旅02
- JAVA之旅03
- JAVA之旅04
- JAVA之旅05
- JAVA之旅06
- JAVA之旅07
- JAVA之旅08
- JAVA之旅09
- java之旅1
- JAVA之旅10
- JAVA之旅11
- JAVA之旅12
- JAVA之旅13
- JAVA之旅14
- JAVA之旅15
- JAVA之旅16
- JAVA之旅17
- JAVA之旅18
- JAVA之旅19
- java之旅2
- JAVA之旅20
- JAVA之旅21
- JAVA之旅22
- JAVA之旅23
- JAVA之旅24
- JAVA之旅25
- JAVA之旅26
- JAVA之旅27
- JAVA之旅28
- JAVA之旅29
- java之旅3
- JAVA之旅30
- JAVA之旅31
- JAVA之旅32
- JAVA之旅33
- JAVA之旅34
- JAVA之旅35
- 1-Java-05辨析
- HashMapArrayMap
- Java8新特性
- Java8接口默认方法
- 图解HashMap(1)
- 图解HashMap(2)
- 2-Android
- 2-Android-1-基础
- View绘制流程
- 事件分发
- AndroidView的事件分发机制和滑动冲突解决
- 自定义View基础
- 1-安卓自定义View基础-坐标系
- 2-安卓自定义View基础-角度弧度
- 3-安卓自定义View基础-颜色
- 自定义View进阶
- 1-安卓自定义View进阶-分类和流程
- 10-安卓自定义View进阶-Matrix详解
- 11-安卓自定义View进阶-MatrixCamera
- 12-安卓自定义View进阶-事件分发机制原理
- 13-安卓自定义View进阶-事件分发机制详解
- 14-安卓自定义View进阶-MotionEvent详解
- 15-安卓自定义View进阶-特殊形状控件事件处理方案
- 16-安卓自定义View进阶-多点触控详解
- 17-安卓自定义View进阶-手势检测GestureDetector
- 2-安卓自定义View进阶-绘制基本图形
- 3-安卓自定义View进阶-画布操作
- 4-安卓自定义View进阶-图片文字
- 5-安卓自定义View进阶-Path基本操作
- 6-安卓自定义View进阶-贝塞尔曲线
- 7-安卓自定义View进阶-Path完结篇伪
- 8-安卓自定义View进阶-Path玩出花样PathMeasure
- 9-安卓自定义View进阶-Matrix原理
- 通用类介绍
- Application
- 2-Android-2-使用
- 2-Android-02控件
- ViewGroup
- ConstraintLayout
- CoordinatorLayout
- 2-Android-03三方使用
- Dagger2
- Dagger2图文完全教程
- Dagger2最清晰的使用教程
- Dagger2让你爱不释手-终结篇
- Dagger2让你爱不释手-重点概念讲解、融合篇
- dagger2让你爱不释手:基础依赖注入框架篇
- 阅读笔记
- Glide
- Google推荐的图片加载库Glide:最新版使用指南(含新特性)
- rxjava
- 这可能是最好的RxJava2.x入门教程完结版
- 这可能是最好的RxJava2.x入门教程(一)
- 这可能是最好的RxJava2.x入门教程(三)
- 这可能是最好的RxJava2.x入门教程(二)
- 这可能是最好的RxJava2.x入门教程(五)
- 这可能是最好的RxJava2.x入门教程(四)
- 2-Android-3-优化
- 优化概况
- 各种优化
- Android端秒开优化
- apk大小优化
- 内存分析
- 混淆
- 2-Android-4-工具
- adb命令
- 一键分析Android的BugReport
- 版本控制
- git
- git章节简述
- 2-Android-5-源码
- HandlerThread 源码分析
- IntentService的使用和源码分析
- 2-Android-9-辨析
- LRU算法
- 什么是Bitmap
- 常见图片压缩方式
- 3-Kotlin
- Kotlin使用笔记1-草稿
- Kotlin使用笔记2
- kotlin特性草稿
- Kotlin草稿-Delegation
- Kotlin草稿-Field
- Kotlin草稿-object
- 4-JavaScript
- 5-Python
- 6-Other
- Git
- Gradle
- Android中ProGuard配置和总结
- gradle使用笔记
- Nexus私服搭建
- 编译提速最佳实践
- 7-设计模式与架构
- 组件化
- 组件化探索(OKR)
- 1-参考列表
- 2-1-组件化概述
- 2-2-gradle配置
- 2-3-代码编写
- 2-4-常见问题
- 2-9-值得一读
- 8-数据结构与算法
- 0临时文件
- 汉诺塔
- 8-数据-1数据结构
- HashMap
- HashMap、Hashtable、HashSet 和 ConcurrentHashMap 的比较
- 迟到一年HashMap解读
- 8-数据-2算法
- 1个就够了
- Java常用排序算法(必须掌握的8大排序算法)
- 常用排序算法总结(性能+代码)
- 必须知道的八大种排序算法(java实现)
- 9-职业
- 阅读
- 书单
- 面试
- 面试-01-java
- Java面试题全集骆昊(上)
- Java面试题全集骆昊(下)
- Java面试题全集骆昊(中)
- 面试-02-android
- 40道Android面试题
- 面试-03-开源源码
- Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程
- 面试-07-设计模式
- 面试-08-算法
- 面试-09-其他
- SUMMARY
- 版权说明
- temp111