我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密。

jdk1.7中的HashMap

在jdk1.8中对HashMap做了很多优化,这里先分析在jdk1.7中的问题,相信大家都知道在jdk1.7多线程环境下HashMap容易出现死循环,这里我们先用代码来模拟出现死循环的情况:

public class HashMapTest {public static void main(String[] args) {HashMapThread thread0 = new HashMapThread();HashMapThread thread1 = new HashMapThread();HashMapThread thread2 = new HashMapThread();HashMapThread thread3 = new HashMapThread();HashMapThread thread4 = new HashMapThread();thread0.start();thread1.start();thread2.start();thread3.start();thread4.start();}
}class HashMapThread extends Thread {private static AtomicInteger ai = new AtomicInteger();private static Map<Integer, Integer> map = new HashMap<>();@Overridepublic void run() {while (ai.get() < 1000000) {map.put(ai.get(), ai.get());ai.incrementAndGet();}}
}

上述代码比较简单,就是开多个线程不断进行put操作,并且HashMap与AtomicInteger都是全局共享的。在多运行几次该代码后,出现如下死循环情形:

其中有几次还会出现数组越界的情况:


这里我们着重分析为什么会出现死循环的情况,通过jps和jstack命名查看死循环情况,结果如下:

从堆栈信息中可以看到出现死循环的位置,通过该信息可明确知道死循环发生在HashMap的扩容函数中,根源在transfer函数中,jdk1.7中HashMap的transfer函数如下:

    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;}}}

总结下该函数的主要作用:

在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。

扩容造成死循环分析过程

前提条件:

这里假设

hash算法为简单的用key mod链表的大小。

最开始hash表size=2,key=3,7,5,则都在table[1]中。

然后进行resize,使size变成4。

未resize前的数据结构如下:

如果在单线程环境下,最后的结果如下:

这里的转移过程,不再进行详述,只要理解transfer函数在做什么,其转移过程以及如何对链表进行反转应该不难。

然后在多线程环境下,假设有两个线程A和B都在进行put操作。线程A在执行到transfer函数中第11行代码处挂起,因为该函数在这里分析的地位非常重要,因此再次贴出来。

此时线程A中运行结果如下:

线程A挂起后,此时线程B正常执行,并完成resize操作,结果如下:

这里需要特别注意的点:由于线程B已经执行完毕,根据Java内存模型,现在newTable和table中的Entry都是主存中最新值:7.next=3,3.next=null。

此时切换到线程A上,在线程A挂起时内存中值如下:e=3,next=7,newTable[3]=null,代码执行过程如下:

newTable[3]=e ----> newTable[3]=3
e=next ----> e=7

此时结果如下:


继续循环:

e=7 next=e.next ----> next=3【从主存中取值】
e.next=newTable[3] ----> e.next=3【从主存中取值】
newTable[3]=e ----> newTable[3]=7
e=next ----> e=3

结果如下:

再次进行循环:

e=3
next=e.next ----> next=null
e.next=newTable[3] ----> e.next=7 即:3.next=7
newTable[3]=e ----> newTable[3]=3
e=next ----> e=null

注意此次循环:e.next=7,而在上次循环中7.next=3,出现环形链表,并且此时e=null循环结束。

结果如下:

在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环,造成悲剧。

扩容造成数据丢失分析过程

遵照上述分析过程,初始时:

线程A和线程B进行put操作,同样线程A挂起:

此时线程A的运行结果如下:

此时线程B已获得CPU时间片,并完成resize操作:

同样注意由于线程B执行完成,newTable和table都为最新值:5.next=null。

此时切换到线程A,在线程A挂起时:e=7,next=5,newTable[3]=null。

执行newtable[i]=e,就将7放在了table[3]的位置,此时next=5。接着进行下一次循环:

e=5
next=e.next ----> next=null,从主存中取值
e.next=newTable[1] ----> e.next=5,从主存中取值
newTable[1]=e ----> newTable[1]=5
e=next ----> e=null

将5放置在table[1]位置,此时e=null循环结束,3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。

jdk1.8中HashMap

在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,这里我们看jdk1.8中HashMap的put操作源码:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null;}

这是jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。

假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。

总结

首先HashMap是线程不安全的,其主要体现:

在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。

【面试题】你知道为什么HashMap是线程不安全的吗?相关推荐

  1. 两个高频设计类面试题:如何设计HashMap和线程池

    你好,我是 yes. 最近在汇总面试题,但是我写的这个版本不是背诵版,不是那种死记硬背刻板的答案. 我的本意是抛砖引玉,针对每个题目给出我自己的理解和解释型的答案,然后背诵版本需要你们自行去总结和记忆 ...

  2. HashMap 为什么线程不安全?

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:developer cnblogs.com/develope ...

  3. 都说 HashMap 是线程不安全的,到底体现在哪儿?

    前言:我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密. 1.jdk1.7中的HashMap 在jdk1.8中对HashM ...

  4. java hashmap非线程安全

    理解和讨论HashMap的线程安全 (2010-06-22 23:00:42) 转载▼ 标签: cache 杂谈 分类: 技术 原贴地址:http://www.javaeye.com/topic/65 ...

  5. java 如何让HashMap变成线程安全的

    我们都知道.HashMap是非线程安全的(非同步的).那么怎么才能让HashMap变成线程安全的呢? 我认为主要可以通过以下三种方法来实现: 1.替换成Hashtable,Hashtable通过对整个 ...

  6. 面试 | HashMap 为什么线程不安全?

    点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | cnblogs.com/developer_c ...

  7. hashmap为什么线程不安全_面试官:你说 HashMap 线程不安全,它为啥不安全呢?...

    扫描下方海报 试读 本文来源: http://cnblogs.com/developer_chan/p/10450908.html 我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但 ...

  8. 最新天猫面试题(含总结):线程池+并发编程+分布式设计+中间件

    最新天猫面试题(含总结):线程池+并发编程+分布式设计+中间件 https://my.oschina.net/u/3892...

  9. JDK1.7和JDK1.8中HashMap是线程不安全的,并发容器ConcurrentHashMap模型

    一.HashMap是线程不安全的 前言 只要是对于集合有一定了解的一定都知道HashMap是线程不安全的,我们应该使用ConcurrentHashMap.但是为什么HashMap是线程不安全的呢,之前 ...

最新文章

  1. Spotify敏捷模式详解三部曲第一篇:研发团队
  2. #define list_entry(ptr, type, member) \   container_of(ptr, type, member)
  3. Web Services简单介绍
  4. python3数据类型:Dictionary(字典)
  5. c#web页面显示弹窗_C#中三种弹出信息窗口的方式
  6. 不擅演讲的马化腾在 08 年讲了什么?
  7. 修改Linux主机名和IP
  8. 二阶龙格库塔公式推导_带你走进最美数学公式
  9. iphonex如何关机_iPhoneX怎么关机 iPhoneX如何强制重启【详细步骤】
  10. 查询所有的oracle表、表空间、数据库连接等信息
  11. linux上的一些命令
  12. 使用cxf3.0.4搭建webservice服务需要的最精简jar包
  13. react native与夜神模拟器结合使用运行安卓平台
  14. 深入Scala系列之一组件重用
  15. HTML嵌套注释、多行注释、多级注释,提高生产力
  16. 学校介绍静态HTML网页设计作品 DIV布局学校官网模板代码 DW大学网站制作成品下载 HTML5期末大作业
  17. 图片转pdf、长图转pdf
  18. 微博、腾讯视频(部分)提取器-- 前端Vue.js篇
  19. Defina脚本 - 恶灵塔任务大全
  20. 数据库设计--实体关系图(ERD)

热门文章

  1. 【转】请尊重测试:测试从业五年有感
  2. 《JavaScript入门经典》学习笔记1
  3. cacti 网络流量监测图形分析工具
  4. 如何到达永生?揭示科学之美
  5. 栅格数据的像素值保存问题
  6. 在Myeclipse里使用Junit
  7. golang经典书籍--Go语言圣经
  8. sprintf、strcpy、strncpy及 memcpy 函数,请问这些函数功能有什么区别?配实例详解!
  9. JUC并发编程六 并发架构--偏向锁
  10. 网络编程2_网络通讯协议, socket(tcp, udp)