Java基础:HashMap假死锁问题的测试、分析和总结
前言
前两天在公司的内部博客看到一个同事分享的线上服务挂掉CPU100%的文章,让我联想到HashMap在不恰当使用情况下的死循环问题,这里做个整理和总结,也顺便复习下HashMap。
直接上测试代码
由于机器配置和性能不同,测试出效果的线程数和put数量也各不相同
public class HashMapInfiniteLoopTest {
/**
* 基于JDK1.7测试HashMap在多线程环境下假死锁的情况
* JDK1.8的HashMap实现跟1.7比较已经有很大的变化,已不存在这样的问题
* (其实这本来不是JDK的一个问题,HashMap本就不是线程安全的,多线程环境下共享一定要用线程安全的Map容器)
*/
public static void main(String[] args) {
String jdkVer = System.getProperty("java.version"); //JDK版本
String jdkMod = System.getProperty("sun.arch.data.model"); //32位还是64位
System.out.println(jdkVer +"#"+ jdkMod);
final Map<String, String> map = new HashMap<>();
// final Map<String, String> map = new ConcurrentHashMap<>();
for(int i=0; i<30; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
for(int j=0; j<1000; j++) {
map.put(""+j+"_"+System.currentTimeMillis(), ""+j+"_"+System.currentTimeMillis());
}
}
}, "myThread_"+i).start();
}
}
}
通过jconsole查看Java进程情况:

最后只能强制结束进程

分析
HashMap使用hash表来作为其底层存储的数据结构(数组下标实现快速索引,链表实现元素碰撞处理),并且支持动态扩容,主要通过resize方法实现,也是从这个方法开始出问题的。(这里有两个面试官喜欢问的点:1.table的默认长度以及扩容前后大小?2.为什么要求table的长度必须是2的N次方?)
因为整个HashMap都不是线程安全的,所以JDK也未对resize方法做同步,如果错误的在多线程环境下共享访问了HashMap就有可能引起我前面提到的假死锁问题。动态扩容的时候需要把旧的链表迁移到新的hash表中,如果是在多线程环境下,可能会形成循环链表,在再次put遍历每个链表检查是否存在相同key时,死循环就出现了(如果是get也会有同样的情况)。
下面是我整理转载自https://coolshell.cn/articles/9606.html的部分内容(写得太好了):
|
1
2
3
4
5
6
7
8
9
10
11
12
|
void resize(int newCapacity){ Entry[] oldTable = table; int oldCapacity = oldTable.length; ...... //创建一个新的Hash Table Entry[] newTable = new Entry[newCapacity]; //将Old Hash Table上的数据迁移到New Hash Table上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor);} |
迁移的源代码,注意高亮处:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
void transfer(Entry[] newTable){ Entry[] src = table; int newCapacity = newTable.length; //下面这段代码的意思是: // 从OldTable里摘一个元素出来,然后放到NewTable中 for (int j = 0; j < src.length; j++) { Entry<K,V> e = src[j]; if (e != null) { src[j] = null; do { Entry<K,V> next = e.next; int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next; } while (e != null); } }} |
- 假设我们的hash算法就是简单的用key mod 一下表的大小(也就是数组的长度)。
- 最上面的是old hash 表,其中的Hash表的size=2, 所以key = 3, 7, 5,在mod 2以后都冲突在table[1]这里了。
- 接下来的三个步骤是Hash表 resize成4,然后所有的<key,value> 重新rehash的过程

并发下的Rehash
1)假设我们有两个线程。我用红色和浅蓝色标注了一下。
我们再回头看一下我们的 transfer代码中的这个细节:
|
1
2
3
4
5
6
7
|
do { Entry<K,V> next = e.next; // <--假设线程一执行到这里就被调度挂起了 int i = indexFor(e.hash, newCapacity); e.next = newTable[i]; newTable[i] = e; e = next;} while (e != null); |
而我们的线程二执行完成了。于是我们有下面的这个样子。

注意,因为Thread1的 e 指向了key(3),而next指向了key(7),其在线程二rehash后,指向了线程二重组后的链表。我们可以看到链表的顺序被反转后。
2)线程一被调度回来执行。
- 先是执行 newTalbe[i] = e;
- 然后是e = next,导致了e指向了key(7),
- 而下一次循环的next = e.next导致了next指向了key(3)

3)一切安好。
线程一接着工作。把key(7)摘下来,放到newTable[i]的第一个,然后把e和next往下移。

4)环形链接出现。
e.next = newTable[i] 导致 key(3).next 指向了 key(7)
注意:此时的key(7).next 已经指向了key(3), 环形链表就这样出现了。

于是,当我们的线程一调用到,HashTable.get(7)时,悲剧就出现了——Infinite Loop。
总结
多线程并发环境下访问共享的map时一定要用线程安全的Map容器,如ConcurrentHashMap,HashTable等。
Java基础:HashMap假死锁问题的测试、分析和总结的更多相关文章
- Java基础-hashMap原理剖析
Java基础-hashMap原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是哈希(Hash) 答:Hash就是散列,即把对象打散.举个例子,有100000条数 ...
- java基础解析系列(九)---String不可变性分析
java基础解析系列(九)---String不可变性分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder java基础解析系列(二)---In ...
- java 关于 hashmap 的实现原理的测试
网上关于HashMap的工作原理的文章多了去了,所以我也不打算再重复别人的文章.我就是有点好奇,我怎么样能更好的理解他的原理,或者说使用他的特性呢?最好的开发就是测试~ 虽说不详讲hashmap的工作 ...
- Java基础之访问文件与目录——测试文件或目录的路径(TryPath)
控制台程序,测试文件或目录的路径. import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.FileSy ...
- Java基础--单例类创建和测试
单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在.单例模式有很多好处,它能够避免实例对象的重复创建,不仅可以减少每次创建对象的时间开销,还可以节约内存空间:能够避免由于操作多个实例导致 ...
- java基础---->hashMap的简单分析(一)
HashMap是一种十分常用的数据结构对象,可以保存键值对.它在项目中用的比较多,今天我们就来学习一下关于它的知识. HashMap的简单使用 一.hashMap的put和get方法 Map<S ...
- java基础hashmap
Iterator中hasNext(), next() 在Iterator类中,我们经常用到两个方法: hasNext(), next(),具体含义: next(), 是返回当前元素, 并指向下一个元 ...
- Java基础学习-Collection体系结构和迭代测试
package Collection; import java.util.ArrayList; import java.util.Collection; import java.util.Iterat ...
- Java基础——HashMap
1.HashMap底层的实现 JDK 1.7 中 HashMap 是以数组+链表的形式组成的 JDK 1.8 之后数组+链表/红黑树的组成的,当链表大于 8 并且容量大于 64 时,链表结构会转换成红 ...
随机推荐
- OJ002
register:这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率.注意是尽可能,不是绝对. 因为,如果定义了很多register变量,可能会超过CPU的寄 ...
- maven_SSM集成的demo
一.集成spring 二.集成springMVC 三.集成mybatis 1. pom.xml <?xml version="1.0" encoding="UTF- ...
- [Swift]LeetCode108. 将有序数组转换为二叉搜索树 | Convert Sorted Array to Binary Search Tree
Given an array where elements are sorted in ascending order, convert it to a height balanced BST. Fo ...
- [Swift]LeetCode630. 课程表 III | Course Schedule III
There are n different online courses numbered from 1 to n. Each course has some duration(course leng ...
- [Swift]LeetCode934. 最短的桥 | Shortest Bridge
In a given 2D binary array A, there are two islands. (An island is a 4-directionally connected grou ...
- Kubernetes 笔记 06 豌豆荚之旅(一)
本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. Hi,大家好, ...
- .net core consul 服务配置 服务发现 服务健康检测 服务变更加载
准备环境 安装consul之后 1. 创建一个.net core webapi 举例为UsercenterService 2. nuget引用Consul组件 https://github.com/ ...
- python之Django学习笔记(三)---URL调度/URL路由
在django中,用户发起url请求消息首先到工程的urls.py中查找是否有匹配的url路径 刚创建好的工程中urls.py只有下面几行代码: from django.contrib import ...
- java之Jvm学习--JVM运行机制
JVM启动流程 1.java虚拟机启动的命令是通过java +xxx(类名,这个类中要有main方法)或者javaw启动的. 2.执行命令后,系统第一步做的就是装载配置,会在当前路径中寻找jvm的co ...
- spring统一错误响应设置
在类入口增加 @RestControllerAdvice注解.可以用于定义@ExceptionHandler.@InitBinder.@ModelAttribute,并应用到所有@RequestMap ...