谈谈HashMap线程不安全的体现
转载自 谈谈HashMap线程不安全的体现
HashMap的原理以及如何实现,之前在JDK7与JDK8中HashMap的实现中已经说明了。
那么,为什么说HashMap是线程不安全的呢?它在多线程环境下,会发生什么情况呢?
1. resize死循环
我们都知道HashMap初始容量大小为16,一般来说,当有数据要插入时,都会检查容量有没有超过设定的thredhold,如果超过,需要增大Hash表的尺寸,但是这样一来,整个Hash表里的元素都需要被重算一遍。这叫rehash,这个成本相当的大。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = ( int )Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1 );
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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;
}
}
}
|
大概看下transfer:
- 对索引数组中的元素遍历
- 对链表上的每一个节点遍历:用 next 取得要转移那个元素的下一个,将 e 转移到新 Hash 表的头部,使用头插法插入节点。
- 循环2,直到链表节点全部转移
- 循环1,直到所有索引数组全部转移
经过这几步,我们会发现转移的时候是逆序的。假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1。这时候就有点头绪了,死锁问题不就是因为1->2的同时2->1造成的吗?所以,HashMap 的死锁问题就出在这个transfer()
函数上。
1.1 单线程 rehash 详细演示
单线程情况下,rehash 不会出现任何问题:
- 假设hash算法就是最简单的 key mod table.length(也就是数组的长度)。
- 最上面的是old hash 表,其中的Hash表的 size = 2, 所以 key = 3, 7, 5,在 mod 2以后碰撞发生在 table[1]
- 接下来的三个步骤是 Hash表 resize 到4,并将所有的
<key,value>
重新rehash到新 Hash 表的过程
如图所示:
1.2 多线程 rehash 详细演示
为了思路更清晰,我们只将关键代码展示出来
1
2
3
4
5
6
|
while ( null != e) {
Entry<K,V> next = e.next;
e.next = newTable[i];
newTable[i] = e;
e = next;
}
|
Entry<K,V> next = e.next;
——因为是单链表,如果要转移头指针,一定要保存下一个结点,不然转移后链表就丢了e.next = newTable[i];
——e 要插入到链表的头部,所以要先用 e.next 指向新的 Hash 表第一个元素(为什么不加到新链表最后?因为复杂度是 O(N))newTable[i] = e;
——现在新 Hash 表的头指针仍然指向 e 没转移前的第一个元素,所以需要将新 Hash 表的头指针指向 ee = next
——转移 e 的下一个结点
假设这里有两个线程同时执行了put()
操作,并进入了transfer()
环节
1
2
3
4
5
6
|
while ( null != e) {
Entry<K,V> next = e.next; //线程1执行到这里被调度挂起了
e.next = newTable[i];
newTable[i] = e;
e = next;
}
|
那么现在的状态为:
从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 rehash 后,就指向了线程2 rehash 后的链表。
然后线程1被唤醒了:
- 执行
e.next = newTable[i]
,于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以e.next = null
, - 执行
newTable[i] = e
,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash 表的 key(3)。好了,e 处理完毕。 - 执行
e = next
,将 e 指向 next,所以新的 e 是 key(7)
然后该执行 key(3)的 next 节点 key(7)了:
- 现在的 e 节点是 key(7),首先执行
Entry<K,V> next = e.next
,那么 next 就是 key(3)了 - 执行
e.next = newTable[i]
,于是key(7) 的 next 就成了 key(3) - 执行
newTable[i] = e
,那么线程1的新 Hash 表第一个元素变成了 key(7) - 执行
e = next
,将 e 指向 next,所以新的 e 是 key(3)
这时候的状态图为:
然后又该执行 key(7)的 next 节点 key(3)了:
- 现在的 e 节点是 key(3),首先执行
Entry<K,V> next = e.next
,那么 next 就是 null - 执行
e.next = newTable[i]
,于是key(3) 的 next 就成了 key(7) - 执行
newTable[i] = e
,那么线程1的新 Hash 表第一个元素变成了 key(3) - 执行
e = next
,将 e 指向 next,所以新的 e 是 key(7)
这时候的状态如图所示:
很明显,环形链表出现了!!当然,现在还没有事情,因为下一个节点是 null,所以transfer()
就完成了,等put()
的其余过程搞定后,HashMap 的底层实现就是线程1的新 Hash 表了。
2. fail-fast
如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。
这个异常意在提醒开发者及早意识到线程安全问题,具体原因请查看ConcurrentModificationException的原因以及解决措施
顺便再记录一个HashMap的问题:
为什么String, Interger这样的wrapper类适合作为键? String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么请这么做吧。因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。
Reference:
1. http://hwl-sz.iteye.com/blog/1897468?utm_source=tuicool&utm_medium=referral
2. http://github.thinkingbar.com/hashmap-infinite-loop/
3. http://www.importnew.com/7099.html
谈谈HashMap线程不安全的体现相关推荐
- HashMap 线程安全问题
前言 我们紧接着上节ArrayList 线程安全问题讲下HashMap的线程安全问题. 之前看书,书中经常会提及.HashTable是线程安全的,HashMap是线程非安全的.在多线程的情况下, Ha ...
- hashmap扩容线程安全问题_HashMap线程不安全的体现
1.多线程put操作后,get操作导致死循环. 2.多线程put非NULL元素后,get操作得到NULL值. 3.多线程put操作,导致元素丢失. 比如一个 ArrayList 类,在添加一个元素的时 ...
- hashmap为什么线程不安全_面试官:你说 HashMap 线程不安全,它为啥不安全呢?...
扫描下方海报 试读 本文来源: http://cnblogs.com/developer_chan/p/10450908.html 我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但 ...
- Java集合:HashMap线程不安全?有哪些表现?
HashMap是线程不安全的!主要表现在多线程情况下: 1)hash冲突时,put方法不是同步的,先存的值会被后存的值覆盖.(1.7和1.8都有的表现) 2)在resize的时候,可能会导致死循环(环 ...
- 学生们喜欢在计算机教室上课用英语怎么说,说说学习本课的感想,并谈谈在信息技术课堂上如何体现以学生为主体,教师为主导?...
通过学习<小学信息技术教学中如何落实"角色转换">这门课程,请结合自己的教学实际,说说学习本课的感想,并谈谈在信息技术课堂上如何体现以学生为主体,教师为主导? 作业二: ...
- java伪唤醒,谈谈JDK线程的伪唤醒
在JDK的官方的wait()方法的注释中明确表示线程可能被"虚假唤醒",JDK也明确推荐使用while来判断状态信息.那么这种情况的发生的可能性有多大呢? 使用生产者消费者模型来说 ...
- 为什么HashMap线程不安全?以及实现HashMap线程安全的解决方案
一.为什么HashMap线程不安全? 原著参考 1.JDK1.7 扩容引发的死循环和数据丢失 (1).当前jdk1.7版本的HashMap线程不安全主要是发生在扩容函数中,其中调用了HshMap的tr ...
- HashMap线程安全问题详细解析
1.简介 HashMap是一种非线程安全的数据结构,即在多线程环境下,无法保证其操作的原子性和一致性.在多个线程同时访问HashMap并进行修改操作时,可能会导致数据的不一致性和线程竞争条件的出现. ...
- 为什么HashMap线程不安全
一.Map概述 我们都知道HashMap是线程不安全的,但是HashMap的使用频率在所有map中确实属于比较高的.因为它可以满足我们大多数的场景了. Map类继承图 上面展示了java中Map的继承 ...
最新文章
- LeetCode:Merge Intervals
- An Introduction To The SQLite C/C++ Interface
- 2022Go安装goimports第三方库命令
- visio篇章--1
- Celery框架简单实例
- Java 文件目录显示
- WAV文件格式终极解析
- 十进制转bcd码c程序语言,bcd码转换成十进制程序函数
- Android开发汇总帖子
- 会议OA项目之我的会议排座批审功能
- 顺序表的基本操作(含全部代码c)
- WiFi开发视频教程分享 | 免费下载
- QQ空间玩吧HTML5游戏引擎使用比例分析
- 域名转换为IP地址示例
- 今日新闻简报 十二条微语早报 每天一分钟 知晓天下事 3月2日
- 第三周 AVI、MP4、WAV文件格式
- 网口灯电路——反相器
- 爱普生Epson LQ-735KII 打印机驱动
- ERLANG日期与时间
- 生物信息学还是跨专业计算机,生物统计学与生物信息学的区别
热门文章
- [Java基础]LinkedList集合的特有功能
- 高等数学上-赵立军-北京大学出版社-题解-练习2.3
- 蓝桥杯2017初赛-分巧克力-二分
- Enlarge GCD CodeForces - 1034A(欧拉筛+最大公约数)
- great English sentence:
- 蓝桥杯 左baby右兄弟
- H - Hello Ms. Ze(树状数组套主席树,线段树上二分)
- 51 NOD 1363 最小公倍数之和 (欧拉函数思维应用)
- CF626F. Bear and Fair Set
- 【每日一题】7月1日题目精讲 借教室