图解集合5:不正确地使用HashMap引发死循环及元素丢失
问题引出
前一篇文章讲解了HashMap的实现原理,讲到了HashMap不是线程安全的。那么HashMap在多线程环境下又会有什么问题呢?
几个月前,公司项目的一个模块在线上运行的时候出现了死循环,死循环的代码就卡在HashMap的get方法上。尽管最终发现不是因为HashMap导致的,但却让我重视了HashMap在多线程环境下会引发死循环的这个问题,下面先用一段代码简单模拟出HashMap的死循环:
public class HashMapThread extends Thread
{
private static AtomicInteger ai = new AtomicInteger(0);
private static Map<Integer, Integer> map = new HashMap<Integer, Integer>(1); public void run()
{
while (ai.get() < 100000)
{
map.put(ai.get(), ai.get());
ai.incrementAndGet();
}
}
}
这个线程的作用很简单,给AtomicInteger不断自增并写入HashMap中,其中AtomicInteger和HashMap都是全局共享的,也就是说所有线程操作的都是同一个AtomicInteger和HashMap。开5个线程操作一下run方法中的代码:
public static void main(String[] args)
{
HashMapThread hmt0 = new HashMapThread();
HashMapThread hmt1 = new HashMapThread();
HashMapThread hmt2 = new HashMapThread();
HashMapThread hmt3 = new HashMapThread();
HashMapThread hmt4 = new HashMapThread();
hmt0.start();
hmt1.start();
hmt2.start();
hmt3.start();
hmt4.start();
}
多运行几次之后死循环就出来了,我大概运行了7次、8次的样子,其中有几次是数组下标越界异常ArrayIndexOutOfBoundsException。这里面要提一点,多线程环境下代码会出现问题并不意味着多线程环境下一定会出现问题,但是只要出现了问题,或者是死锁、或者是死循环,那么你的项目除了重启就没有什么别的办法了,所以代码的线程安全性在开发、评审的时候必须要重点考虑到。OK,看一下控制台:
红色方框一直亮着,说明代码死循环了。死循环问题的定位一般都是通过jps+jstack查看堆栈信息来定位的:
看到Thread-0处于RUNNABLE,而从堆栈信息上应该可以看出,这次的死循环是由于Thread-0对HashMap进行扩容而引起的。
所以,本文就解读一下,HashMap的扩容为什么会引起死循环。
正常的扩容过程
先来看一下HashMap一次正常的扩容过程。简单一点看吧,假设我有三个经过了最终rehash得到的数字,分别是5 7 3,HashMap的table也只有2,那么HashMap把这三个数字put进数据结构了之后应该是这么一个样子的:
这应该很好理解。然后看一下resize的代码,上面的堆栈里面就有:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
} Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
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);
}
}
}
我总结一下这三段代码,HashMap一次扩容的过程应该是:
- 取当前table的2倍作为新table的大小
- 根据算出的新table的大小new出一个新的Entry数组来,名为newTable
- 轮询原table的每一个位置,将每个位置上连接的Entry,算出在新table上的位置,并以链表形式连接
- 原table上的所有Entry全部轮询完毕之后,意味着原table上面的所有Entry已经移到了新的table上,HashMap中的table指向newTable
这样就完成了一次扩容,用图表示是这样的:
HashMap的一次正常扩容就是这样的,这很好理解。
扩容导致的死循环
既然是扩容导致的死循环,那么继续看扩容的代码:
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
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);
}
}
}
两个线程,线程A和线程B。假设第9行执行完毕,线程A切换,那么对于线程A而言,是这样的:
CPU切换到线程B运行,线程B将整个扩容过程全部执行完毕,于是就形成了:
此时CPU切换到线程A上,执行第8行~第14行的do...while...循环,首先放置3这个Entry:
我们必须要知道,由于线程B已经执行完毕,因此根据Java内存模型(JMM),现在table里面所有的Entry都是最新的,也就是7的next是3,3的next是null。3放置到table[3]的位置上了,下面的步骤是:
- e=next,即e=7
- 判断e不等于null,循环继续
- next=e.next,即next=7的next,也就是3
- 放置7这个Entry
所以,用图表示就是:
放置完7之后,继续运行代码:
- e=next,也就是说e=3
- 判断e不等于null,循环继续
- next=e.next,即3的next,也就是null
- 放置3这个Entry
把3移到table[3]上去,死循环就出来了:
3移到table[3]上去了,3的next指向7,由于原先7的next指向3,这样就成了一个死循环。
此时执行13行的e=next,那么e=null,循环终止。尽管此次循环确实结束了,但是后面的操作,只要涉及轮询HashMap数据结构的,无论是迭代还是扩容,都将在table[3]这个链表处出现死循环。这也就是前面的死循环堆栈出现的原因,transfer的484行,因为这是一次扩容操作,需要遍历HashMap数据结构,transfer方法是扩容的最后一个方法。
3 5 7又会有怎样的结果
可能有人觉得上面的数字5 7 3太巧了,像是专门为了产生HashMap的死循环而故意选择的数字。
这个问题,我这么回答:我记得在《从Paxos到Zookeeper分布式一致性原理与实践》有一段话大概是这么描述的,有一个被反复实践得出的结论是,任何在多线程下可能发生的错误场景最终一定会发生。
5 7 3这个数字可不巧,扩容前相邻两个Entry被分配到扩容后同样的table位置是很正常的。关键的是,即使这种异常场景发生的可能性再低,只要发生一次,那么你的系统就部分甚至全部不可用了----除了重启系统没有任何办法。所以,这种可能会发生的异常场景必须提前扼杀。
OK,不扯了,前面讲了5 7 3导致了死循环,现在看一下正常的顺序3 5 7,会发生什么问题。简单看一下,就不像上面讲得这么详细了:
这是扩容前数据结构中的内容,扩容之后正常的应该是:
现在在多线程下遇到问题了,某个线程先放7:
再接着放5:
由于5的next此时为null,因此扩容操作结束,3 5 7造成的结果就是元素丢失。
如何解决
把一个线程非安全的集合作为全局共享的,本身就是一种错误的做法,并发下一定会产生错误。
所以,解决这个问题的办法很简单,有两种:
1、使用Collections.synchronizedMap(Mao<K,V> m)方法把HashMap变成一个线程安全的Map
2、使用Hashtable、ConcurrentHashMap这两个线程安全的Map
不过,既然选择了线程安全的办法,那么必然要在性能上付出一定的代价----毕竟这个世界上没有十全十美的事情,既要运行效率高、又要线程安全。
图解集合5:不正确地使用HashMap引发死循环及元素丢失的更多相关文章
- 集合(五)不正确地使用HashMap引发死循环及元素丢失
前一篇文章讲解了HashMap的实现原理,讲到了HashMap不是线程安全的.那么HashMap在多线程环境下又会有什么问题呢? 几个月前,公司项目的一个模块在线上运行的时候出现了死循环,死循环的代码 ...
- 图解集合4:HashMap
初识HashMap 之前的List,讲了ArrayList.LinkedList,最后讲到了CopyOnWriteArrayList,就前两者而言,反映的是两种思想: (1)ArrayList以数组形 ...
- Java 集合系列14之 Map总结(HashMap, Hashtable, TreeMap, WeakHashMap等使用场景)
概要 学完了Map的全部内容,我们再回头开开Map的框架图. 本章内容包括:第1部分 Map概括第2部分 HashMap和Hashtable异同第3部分 HashMap和WeakHashMap异同 转 ...
- JDK(九)JDK1.7源码分析【集合】HashMap的死循环
前言 在JDK1.7&1.8源码对比分析[集合]HashMap中我们遗留了一个问题:为什么HashMap在调用resize() 方法时会出现死循环?这篇文章就通过JDK1.7的源码来分析并解释 ...
- java集合框架(一):HashMap
有大半年没有写博客了,虽然一直有在看书学习,但现在回过来看读书基本都是一种知识“输入”,很多时候是水过无痕.而知识的“输出”会逼着自己去找出没有掌握或者了解不深刻的东西,你要把一个知识点表达出来,自己 ...
- java集合初探(一):HashMap.
一.概述 HashMap可能是我们最经常用的Map接口的实现了.话不多说,我们先看看HashMap类的注释: 基于哈希表的Map接口实现. 这个实现提供了所有可选的映射操作,并允许空值和空键.(Has ...
- Java 数据类型:集合接口Map:HashTable;HashMap;IdentityHashMap;LinkedHashMap;Properties类读取配置文件;SortedMap接口和TreeMap实现类:【线程安全的ConcurrentHashMap】
Map集合java.util.Map Map用于保存具有映射关系的数据,因此Map集合里保存着两个值,一个是用于保存Map里的key,另外一组值用于保存Map里的value.key和value都可以是 ...
- 深入理解JAVA集合系列三:HashMap的死循环解读
由于在公司项目中偶尔会遇到HashMap死循环造成CPU100%,重启后问题消失,隔一段时间又会反复出现.今天在这里来仔细剖析下多线程情况下HashMap所带来的问题: 1.多线程put操作后,get ...
- 多线程下HashMap的死循环问题
多线程下[HashMap]的问题: 1.多线程put操作后,get操作导致死循环.2.多线程put非NULL元素后,get操作得到NULL值.3.多线程put操作,导致元素丢失. 本次主要关注[Has ...
随机推荐
- AspJpeg使用 .
下载ASPJpeg的bll库,引用 一.为图片添加水印//实例化组件ASPJPEGLib.IASPJpeg objJpeg = new ASPJPEGLib.ASPJpeg();//打开源图片文件ob ...
- MySQL安装之后没有MySQL数据库的原因
mysql安装完之后,登陆后发现只有两个数据库:mysql> show databases;+--------------------+| Database |+------ ...
- asp.net下AjaxMethod的使用方法
使用AjaxMethod可以在客户端异步调用服务端方法,简单地说就是在JS里调用后台.cs文件里的方法, 做一些JS无法做到的操作,如查询数据库 使用AjaxMethod要满足一下几点: 1.如果还没 ...
- Web程序的运行原理及流程(一)
自己做Web程序的开发也有两年多了 从最开始跟风学框架 到第一用上框架的欣喜若狂 我相信每个程序员都是这样过来的 在大学学习一门语言 学会后往往很想做一个实际的项目出来 我当时第一次做WEB项目看 ...
- Linux下GDB调试
GDB 是一个强大的命令行调试工具.大家知道命令行的强大就是在于,其可以形成执行 序列,形成脚本.UNIX 下的软件全是命令行的,这给程序开发提供了极大的便利,命令行 软件的优势在于, 他们可以非常容 ...
- Oracle直方图的详细解析
yuanwen:http://blog.csdn.net/javacoffe/article/details/5578206 Oracle直方图解析 一. 何谓直方图: 直方图是一种统计学上的工 ...
- 新版本MySQL Server 5.7的免安装版本设置
今天重新配置电脑,安装java开发运行的相关环境,在安装mysql的过程中,遇到了一些问题. 因为在网站上下载的是免安装版本的mysql 5.7 ,所以在安装过程中只需要解压缩zip的压缩包即可. 之 ...
- 表单验证插件 - formValidator
表单验证插件 - formValidator * 引入formValidator插件文件 * 引入formValidator插件的主文件 * 引入formValidator插件的正则有关文件 * 引入 ...
- 【Mail】搭建邮件服务器(LAMP+Postfix+Dovcot+PostfixAdmin+Roundcubemail)
大纲 一.mail部署说明 二.安装准备 三.LMAP环境配置 四.配置postfixadmin 五.配置postfix 六.配置dovecot 七.测试SMTP和POP3服务 八.配置Roundcu ...
- JSON字符串解析
有时保存在数据库的数据是一串json字符串,需要进行读取的时候就需要解析操作. 简单介绍两种: 1.net.sf.json.* 2.com.alibaba.fastjson.* 需要的包自行下载. 第 ...