- 浏览: 395375 次
- 性别:
- 来自: 杭州
博客专栏
-
Spring技术内幕读书笔...
浏览量:15658
文章分类
最新评论
-
albert0707:
非常感谢!
JAVA NIO 实例 -
greatwqs:
表露清晰, 言简意赅, 重新温习了一次
MySQL事务隔离级别详解 -
lgh1992314:
新版本都不需要了。ServiceLoader<Drive ...
DriverManager和Class.forName()的异同 -
zipmoon:
像以上的实验,就没有出现数据幻读的问题。 ---原因:My ...
MySQL事务隔离级别详解 -
jnjeC:
贴代码来了。大家还要根据代码做一定的修改。########## ...
MySQL事务隔离级别详解
本文受http://pt.alibaba-inc.com/wp/dev_related_969/hashmap-result-in-improper-use-cpu-100-of-the-problem-investigated.html 的启发,引用了其中的思想,对此表示感谢。
来到杭州实习有一段日子了,很长时间都没有更新博客了,前几天,闲来无事,随便翻了一本书,毕玄的《分布式JAVA应用》,在看到HashMap那一节的时候,其中提到了HashMap是非线程安全的,在并发场景中如果不保持足够的同步,就有可能在执行HashMap.get时进入死循环,将CPU的消耗到100%。HashMap是线程不安全的,这个我知道的,但是在get操作会出现死循环,我还是第一次听说到。于是我google了一下,网上讨论的很多,原来很多人对这个都感兴趣啊,于是我深入到HashMap的源码去探究了一下。
大家都知道,HashMap采用链表解决Hash冲突,具体的HashMap的分析可以参考一下http://zhangshixi.iteye.com/blog/672697 的分析。因为是链表结构,那么就很容易形成闭合的链路,这样在循环的时候就会产生死循环。但是,我好奇的是,这种闭合的链路是如何形成的呢。在单线程情况下,只有一个线程对HashMap的数据结构进行操作,是不可能产生闭合的回路的。那就只有在多线程并发的情况下才会出现这种情况,那就是在put操作的时候,如果size>initialCapacity*loadFactor,那么这时候HashMap就会进行rehash操作,随之HashMap的结构就会发生翻天覆地的变化。很有可能就是在两个线程在这个时候同时触发了rehash操作,产生了闭合的回路。下面我们从源码中一步一步地分析这种回路是如何产生的。先看一下put操作:
public V put(K key, V value) { if (key == null) return putForNullKey(value); int hash = hash(key.hashCode()); int i = indexFor(hash, table.length); //存在key,则替换掉旧的value for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; //table[i]为空,这时直接生成一个新的entry放在table[i]上 addEntry(hash, key, value, i); return null; }
addEntry操作:
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); }
可以看到,如果现在size已经超过了threshold,那么就要进行resize操作:
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]; //将旧的Entry数组的数据转移到新的Entry数组上 transfer(newTable); table = newTable; threshold = (int)(newCapacity * loadFactor); }
看一下transfer操作,闭合的回路就是在这里产生的:
void transfer(Entry[] newTable) { Entry[] src = table; int newCapacity = newTable.length; /* * 在转换的过程中,HashMap相当于是把原来链表上元素的的顺序颠倒了。 * 比如说 原来某一个Entry[i]上链表的顺序是e1->e2->null,那么经过操作之后 * 就变成了e2->e1->null */ 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); } } }
那么回路究竟是如何产生的呢,问题就出在next=e.next这个地方,在多线程并发的环境下,为了便于分析,我们假设就两个线程P1,P2。src[i]的链表顺序是e1->e2->null。我们分别线程P1,P2的执行情况。
首先,P1,和P2进入到了for循环中,这时候在线程p1和p2中,局部变量分别如下:
e | next | |
P1 | e1 | e2 |
P2 | e1 | e2 |
此时两个Entry的顺序是依然是最开始的状态e1->e2->null, 但是此时p1可能某些原因线程暂停了,p2则继续执行,并执行完了do while循环。这时候Entry的顺序就变成了e2->e1->null。在等到P2执行完之后,可能p1才继续执行,这时候在P1线程中局部变量e的值为e1,next的值为e2(注意此时两个元素在内存中的顺序变成了e2->e1->null),下面P1线程进入了do while循环。这时候P1线程在新的Entry数组中找到e1的位置,
e.next = newTable[i]; newTable[i] = e;
下面会把next赋值给e,这时候e的值成为了e2,继续下一次循环,这时候
e | next | |
P1 | e2 | e1 |
e2->next=e1,这个是线程P2的"功劳"。程序执行完这次循环之后,e=e1,
继续第三次循环,这时候根据算法,就会进行e1->next=e2。
这样在线程P1中执行了 e1->next=e2,在线程P2中执行了 e2->next=e1,这样就形成了一个环。在get操作的时候,next值永远不为null,造成了死循环。
实际上,刚开始我碰到这个说法的时候,还被吓了一跳,HashMap怎么还会出现这个问题呢,仔细分析一下,这个问题再高并发的场景下是很容易出现的。Sun的工程师建议在这样的场景下应采用ConcurrentHashMap。具体参考http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6423457 。
虽然这个问题再平时的工作中还没有遇到,但是以后需要注意。要在不同的场景下选择合适的类,规避类似HashMap这种死循环的问题。
评论
确实是,母猴一洗!!一起没注意!
刚去查了一下,才知道什么叫做月经贴,out了。
师兄在哪个部门?有时间交流一下!
你这位师兄就是人称校长的那位
哦,他就是传说中的校长啊
刚去查了一下,才知道什么叫做月经贴,out了。
师兄在哪个部门?有时间交流一下!
你这位师兄就是人称校长的那位
hashTable?
应该是Hashtable,这玩意首先命名不规范,其次是效率一般,并且k\v不能是null,此物多存在于那些陈旧的历史代码里。
现在开发,请使用ConcurrentHashMap!
<p> </p>
Entry[] src = table;
int newCapacity = newTable.length;
/*
* 在转换的过程中,HashMap相当于是把原来链表上元素的的顺序颠倒了。
* 比如说 原来某一个Entry[i]上链表的顺序是e1->e2->null,那么经过操作之后
* 就变成了e2->e1->null
*/
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的put方法: public V put(K key, V value) { if (key == null) return putForNullKey(value); [color=red]int hash = hash(key.hashCode());[/color] int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; [color=red]addEntry(hash, key, value, i);[/color] return null; }
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new [color=red]Entry<K,V>(hash, key, value, e);[/color] if (size++ >= threshold) resize(2 * table.length); }
Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; [color=red]hash = h;[/color] }
static int indexFor(int h, int length) { return h & (length-1); } static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
以下是我测试indexFor的代码和结果,不知道问题出在哪:
Entry[] newTable = new Entry[3]; int newCapacity = newTable.length; System.out.println(hash("p1".hashCode())+"-->"+indexFor(hash("p1".hashCode()), newCapacity)); System.out.println(hash("p2".hashCode())+"-->"+indexFor(hash("p2".hashCode()), newCapacity)); System.out.println(hash("p3".hashCode())+"-->"+indexFor(hash("p3".hashCode()), newCapacity));
结果:3334-->2
3333-->0
3332-->0
实际上,因为HashMap可以存放(null,value)即key可以是null,在resize的时候,所以需要遍历一边,否则,你怎么判断这个table[i]上到底有没有元素呢?
找到原因了:)
e.next = newTable[i]; newTable[i] = e;就是把key通过hash算法和indexFor算法后的到的下标放到同一个链中,比如key="p1"和key="p2"通过“indexFor(hash("p1".hashCode()), newCapacity)”后如果他们的下标都是0时,那么他的存储结构是Entry[0]=Entry(hash,"p1","p1",Entry(hash,"p2","p2",null));
倒序排列的就是p1和p2的顺序,那么代码
e.next = newTable[i]; newTable[i] = e;对Entry[0]中元素的操作我们可以简写成
e.next = e; 直到e为null,就是这样,原本的顺序是e1|e1.next-->e2|e2.next-->null 转换后 null<--e1.next|e1<--e2.next|e2如果在多线程下操作(我模拟的程序结果是有这样一种情况)e2<--e1.next|e1<--e2.next|e2,在Entry数组的某个位置上出现循环链表。
Entry[] src = table;
int newCapacity = newTable.length;
/*
* 在转换的过程中,HashMap相当于是把原来链表上元素的的顺序颠倒了。
* 比如说 原来某一个Entry[i]上链表的顺序是e1->e2->null,那么经过操作之后
* 就变成了e2->e1->null
*/
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的put方法: public V put(K key, V value) { if (key == null) return putForNullKey(value); [color=red]int hash = hash(key.hashCode());[/color] int i = indexFor(hash, table.length); for (Entry<K,V> e = table[i]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; [color=red]addEntry(hash, key, value, i);[/color] return null; }
void addEntry(int hash, K key, V value, int bucketIndex) { Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new [color=red]Entry<K,V>(hash, key, value, e);[/color] if (size++ >= threshold) resize(2 * table.length); }
Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; [color=red]hash = h;[/color] }
static int indexFor(int h, int length) { return h & (length-1); } static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
以下是我测试indexFor的代码和结果,不知道问题出在哪:
Entry[] newTable = new Entry[3]; int newCapacity = newTable.length; System.out.println(hash("p1".hashCode())+"-->"+indexFor(hash("p1".hashCode()), newCapacity)); System.out.println(hash("p2".hashCode())+"-->"+indexFor(hash("p2".hashCode()), newCapacity)); System.out.println(hash("p3".hashCode())+"-->"+indexFor(hash("p3".hashCode()), newCapacity));
结果:3334-->2
3333-->0
3332-->0
实际上,因为HashMap可以存放(null,value)即key可以是null,在resize的时候,所以需要遍历一边,否则,你怎么判断这个table[i]上到底有没有元素呢?
找到原因了:)
e.next = newTable[i]; newTable[i] = e;就是把key通过hash算法和indexFor算法后的到的下标放到同一个链中,比如key="p1"和key="p2"通过“indexFor(hash("p1".hashCode()), newCapacity)”后如果他们的下标都是0时,那么他的存储结构是Entry[0]=Entry(hash,"p1","p1",Entry(hash,"p2","p2",null));
倒序排列的就是p1和p2的顺序,那么代码
e.next = newTable[i]; newTable[i] = e;对Entry[0]中元素的操作我们可以简写成
e.next = e; 直到e为null,就是这样,原本的顺序是e1|e1.next-->e2|e2.next-->null 转换后 null<--e1.next|e1<--e2.next|e2如果在多线程下操作(我模拟的程序结果是有这样一种情况)e2<--e1.next|e1<--e2.next|e2,在Entry数组的某个位置上出现循环链表。
只要保证put的是同步的,就不会有这个问题了。
另外rehash是比较耗资源的事情,在一开始设置好容量,尽量不rehash.
实在不行,直接用ConcurrentHashMap。
楼主的文章总结的很好。
相关推荐
Java HashMap的死循环原因分析 HashMap是Java中一种常用的数据结构,它提供了快速的查找、插入和删除操作。然而,在多线程环境中使用HashMap可能会导致死循环的问题。下面我们来分析HashMap的死循环原因。 首先,...
详 解 hashmap 1.7 扩 容 机 制 的 数 据 迁 移 以 及 出 现 环 形 列 表 导 致 死 锁 情 况 视 频
然而,由于HashMap不是线程安全的,因此在多线程环境下直接使用可能会引发一系列问题,其中之一就是所谓的"死循环"。本文将深入探讨这一主题。 首先,死循环问题通常发生在并发环境中,当多个线程同时修改HashMap时...
本文将结合马士兵老师的讲解,详细阐述HashMap在不同JDK版本中的put操作流程,以及可能遇到的死循环问题。 首先,我们来看JDK 8中HashMap的put操作。在JDK 8中,HashMap进行了重大的优化,引入了红黑树(Red-Black ...
《HashMap面试题详解》 HashMap作为Java集合框架中的重要成员,是面试中常见的知识点,尤其在数据结构与算法、并发编程以及JVM内存管理等领域,HashMap的深入理解至关重要。本篇将围绕HashMap的相关面试题,从基础...
Java HashMap的死循环是一个在多线程环境下容易出现的问题,主要由于其内部的迭代器实现方式和并发操作不当引起。本文将深入分析这个问题,并探讨如何避免这类错误。 首先,Java HashMap在非线程安全的环境下,如果...
### HashMap介绍和使用详解 #### 一、HashMap的数据结构 HashMap是Java集合框架的一个重要组成部分,它实现了Map接口,能够存储键值对映射。在Java编程语言中,最基本的数据结构有两种:数组和引用(模拟指针)。...
在多线程环境下,两个线程同时触发扩容可能导致循环链表的形成,从而引发死循环,这是一种严重的性能问题。 为了解决HashMap的线程不安全问题,我们可以采取以下几种策略: 1. 使用Collections.synchronizedMap()...
哈希映射(HashMap)是Java编程语言中广泛使用的数据结构之一,主要提供键值对的存储和查找功能。HashMap的实现基于哈希表的概念,它通过计算对象的哈希码来快速定位数据,从而实现了O(1)的平均时间复杂度。在深入...
HashMap之resize()方法源码解读 HashMap的resize()方法是HashMap中最核心的方法之一,该方法负责扩容HashMap的容量,以便存储更多的键值对。下面我们将对HashMap的resize()方法进行源码解读,了解其扩容机制和原理...
该方法首先判断 key 是否为 null,然后根据 key 的 hashCode 值计算 Hash 值,接着搜索指定 hash 值在对应 table 中的索引,如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素,如果找到指定 ...
java7 hashmap源码 随着Java学习的不断深入,发现...多线程下,hashmap的resize()方法为什么容易出现死循环? 答: 其他面试题? 答: 并发 概述 :star::star: :star::star: 线程池 :star: AQS :star: 锁 ListenalbeFut
HashMap是Java编程语言中一个非常重要的数据结构,它属于集合框架的一部分,主要用于存储键值对(Key-Value)数据。HashMap在内部实现上基于哈希表,也称为散列表,它提供了一种快速查找、插入和删除数据的方法,...
### HashMap与HashTable的区别详解 #### 引言 在Java编程中,`HashMap`与`HashTable`作为两种常用的数据结构,经常被用来存储键值对数据。尽管它们在功能上相似,但在实现细节、性能表现以及使用场景方面存在显著...
《HashMap 实例解析与关联数据结构对比》 HashMap 是 Java 中常用的一种数据结构,属于 Java.util 包下的类,它是基于哈希表实现的。在本文中,我们将深入理解 HashMap 的实例及其工作原理,并与其他数据结构如 ...
HashMap是Java编程语言中常用的集合类之一,它属于哈希表数据结构,提供key-value的存储方式,并且具有快速查询的特性。然而,HashMap本身并不保证元素的顺序,特别是当涉及到遍历或输出HashMap的内容时,顺序可能会...
HashMap 总结 HashMap 是 Java 中的一种常用的数据结构,用于存储键值对(Key-Value)数据。下面是 HashMap 的一些特性和使用方法总结。 键(Key)的特性 1. 键可以为 null:HashMap 中的键可以为 null,这意味着...
### hashMap和hashTable的区别 #### 一、简介与基本概念 `HashMap` 和 `HashTable` 都是 Java 集合框架中非常重要的数据结构,它们都实现了 `Map` 接口,用于存储键值对。尽管它们在功能上有很多相似之处,但在...