hashmap put过程_面试官:HashMap 为什么线程不安全?
点击上方“linkoffer”,
选择关注公众号高薪职位第一时间送达
- 1.jdk1.7中的HashMap
- 1.1 扩容造成死循环分析过程
- 1.2 扩容造成数据丢失分析过程
- 2.jdk1.8中HashMap
- 总结
前言:我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但是其线程不安全主要体现在什么地方呢,本文将对该问题进行解密。
1.jdk1.7中的HashMap
在jdk1.8中对HashMap做了很多优化,这里先分析在jdk1.7中的问题,相信大家都知道在jdk1.7多线程环境下HashMap容易出现死循环,这里我们先用代码来模拟出现死循环的情况:
1 public class HashMapTest {23 public static void main(String[] args) {4 HashMapThread thread0 = new HashMapThread();5 HashMapThread thread1 = new HashMapThread();6 HashMapThread thread2 = new HashMapThread();7 HashMapThread thread3 = new HashMapThread();8 HashMapThread thread4 = new HashMapThread();9 thread0.start();10 thread1.start();11 thread2.start();12 thread3.start();13 thread4.start();14 }15 }1617 class HashMapThread extends Thread {18 private static AtomicInteger ai = new AtomicInteger();19 private static Map map = new HashMap<>();2021 @Override22 public void run() {23 while (ai.get() < 1000000) {24 map.put(ai.get(), ai.get());25 ai.incrementAndGet();26 }27 }28 }
上述代码比较简单,就是开多个线程不断进行put操作,并且HashMap与AtomicInteger都是全局共享的。在多运行几次该代码后,出现如下死循环情形:
其中有几次还会出现数组越界的情况:
这里我们着重分析为什么会出现死循环的情况,通过jps和jstack命名查看死循环情况,结果如下:
从堆栈信息中可以看到出现死循环的位置,通过该信息可明确知道死循环发生在HashMap的扩容函数中,根源在transfer函数中,jdk1.7中HashMap的transfer函数如下:
1 void transfer(Entry[] newTable, boolean rehash) {2 int newCapacity = newTable.length;3 for (Entry e : table) {4 while(null != e) {5 Entry next = e.next;6 if (rehash) {7 e.hash = null == e.key ? 0 : hash(e.key);8 }9 int i = indexFor(e.hash, newCapacity);10 e.next = newTable[i];11 newTable[i] = e;12 e = next;13 }14 }15 }
总结下该函数的主要作用:
在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。下面进行详细分析。
1.1 扩容造成死循环分析过程
前提条件:
这里假设
#1.hash算法为简单的用key mod链表的大小。
#2.最开始hash表size=2,key=3,7,5,则都在table[1]中。
#3.然后进行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]=3e=next ----> e=7
此时结果如下:
继续循环:
e=7next=e.next ----> next=3【从主存中取值】e.next=newTable[3] ----> e.next=3【从主存中取值】newTable[3]=e ----> newTable[3]=7e=next ----> e=3
结果如下:
再次进行循环:
e=3next=e.next ----> next=nulle.next=newTable[3] ----> e.next=7 即:3.next=7newTable[3]=e ----> newTable[3]=3e=next ----> e=null
注意此次循环:e.next=7,而在上次循环中7.next=3,出现环形链表,并且此时e=null循环结束。
结果如下:
在后续操作中只要涉及轮询hashmap的数据结构,就会在这里发生死循环,造成悲剧。
1.2 扩容造成数据丢失分析过程
遵照上述分析过程,初始时:
线程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=5next=e.next ----> next=null,从主存中取值e.next=newTable[1] ----> e.next=5,从主存中取值newTable[1]=e ----> newTable[1]=5e=next ----> e=null
将5放置在table[1]位置,此时e=null循环结束,3元素丢失,并形成环形链表。并在后续操作hashmap时造成死循环。
2.jdk1.8中HashMap
在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全,这里我们看jdk1.8中HashMap的put操作源码:
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,2 boolean evict) {3 Node[] tab; Node p; int n, i;4 if ((tab = table) == null || (n = tab.length) == 0)5 n = (tab = resize()).length;6 if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素7 tab[i] = newNode(hash, key, value, null);8 else {9 Node e; K k;10 if (p.hash == hash &&11 ((k = p.key) == key || (key != null && key.equals(k))))12 e = p;13 else if (p instanceof TreeNode)14 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);15 else {16 for (int binCount = 0; ; ++binCount) {17 if ((e = p.next) == null) {18 p.next = newNode(hash, key, value, null);19 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st20 treeifyBin(tab, hash);21 break;22 }23 if (e.hash == hash &&24 ((k = e.key) == key || (key != null && key.equals(k))))25 break;26 p = e;27 }28 }29 if (e != null) { // existing mapping for key30 V oldValue = e.value;31 if (!onlyIfAbsent || oldValue == null)32 e.value = value;33 afterNodeAccess(e);34 return oldValue;35 }36 }37 ++modCount;38 if (++size > threshold)39 resize();40 afterNodeInsertion(evict);41 return null;42 }
这是jdk1.8中HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
这里只是简要分析下jdk1.8中HashMap出现的线程不安全问题的体现,后续将会对java的集合框架进行总结,到时再进行具体分析。
总结
首先HashMap是线程不安全的,其主要体现:
#1.在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失。
#2.在jdk1.8中,在多线程环境下,会发生数据覆盖的情况。
hashmap put过程_面试官:HashMap 为什么线程不安全?相关推荐
- java类加载过程_面试官:java类的加载过程
Java 类加载机制 类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段. 其中类加载过程包括加载.验证.准备.解析和初始化五个阶段. 类的加载 ...
- java进程内存一直没释放_面试官:一个线程OOM,进程里其他线程还能运行么?...
引言 这题是一个网友@大脸猫爱吃鱼给我的提问,出自今年校招美团三面的一个真题.大致如下 一个进程有3个线程,如果一个线程抛出oom,其他两个线程还能运行么? 先说一下答案,答案是还能运行 不瞒大家说, ...
- redis怎么修改_面试官问我Redis事务,还问我有哪些实现方式
❝ 「第12期」 距离大叔的80期小目标还有68期,今天大叔要跟大家分享的内容是 -- Reids中的事务.同样,这也是redis中重要指数为四颗星的必备基础知识点.下面一起来了解一下吧. ❞ 相信大 ...
- java 面试题 由浅入深_面试官由浅入深的面试套路
阅读文本大概需要3分钟. 从上图看来面试官面试是有套路的,一不小心就一直被套路. 0x01:Thread 面试官 :创建线程有哪几种方式? 应聘者 :继承Thread类.实现Runable接口.使用j ...
- hashmap put过程_阿里面试官:HashMap数据结构之道
问题1:HashMap的数据结构是什么样的? 同学1:嗯...数组+链表 同学2:数组+链表... 同学3:数组+链表... 同学4:数组+链表+红黑树... 同学n:..... 为什么答案会有两种? ...
- 哈希表查找失败的平均查找长度_面试官:哈希表都不知道,你是怎么看懂HashMap的?...
本文作者 作者:马可没有菠萝 链接: https://juejin.im/post/6876105622274703368 本文由作者授权发布. HashMap是Java面试中的必问考点之一,网上关于 ...
- hashmap扩容_面试官问:HashMap在并发情况下为什么造成死循环?一脸懵
这个问题是在面试时常问的几个问题,一般在问这个问题之前会问Hashmap和HashTable的区别?面试者一般会回答:hashtable是线程安全的,hashmap是线程不安全的. 那么面试官就会紧接 ...
- hashmap为什么线程不安全_面试官:你说 HashMap 线程不安全,它为啥不安全呢?...
扫描下方海报 试读 本文来源: http://cnblogs.com/developer_chan/p/10450908.html 我们都知道HashMap是线程不安全的,在多线程环境中不建议使用,但 ...
- hashmap put过程_看完还不懂HashMap算我输(附互联网大厂面试常见问题)
HashMap的原理与实现 版本之更迭: –>JDK 1.7 : Table数组+ Entry链表: –>JDK1.8 : Table数组+ Entry链表/红黑树:(为什么要使用红黑树? ...
最新文章
- 仙道服务器维护,5月22日8:00全区停机维护 刺金传说上线
- opencv---JPEG图像质量检测代码
- mac版python连接mysql_Mac下Python连接MySQL · BlBana’s BlackHouse
- python qt教程_Python - Python Qt 开发教程(1)
- 科学小世界,婚姻大殿堂
- python openstack rabbitmq_OpenStack--Rabbitmq组件消息队列
- 通过Ajax方式上传文件(input file),使用FormData进行Ajax请求
- kafka分区与分组原理_大数据技术-Kafka入门
- SPRING IN ACTION 第4版笔记-第二章-001-用@Autowired\@ComponentScan、@Configuration、@Component实现自动装载bean...
- angular 注入器配置_注入器和发布库–AngularJS学习笔记(三)
- 掌阅科技前三季度净利润1.66亿元 同比增长53.75%
- spring中context:property-placeholder
- 使用Hadoop搭建现代电信企业架构
- 小米蓝牙音响驱动_拆解报告:小米无线充蓝牙音箱
- 文章目录---收藏不迷路
- android自定义速度仪表盘,自定义View实战:汽车速度仪表盘
- 3.抽象类:什么是抽象类???抽象类的特点有哪些???
- 三菱FX5U位逻辑指令
- ubuntu出现有线已连接却无法上网的解决方法(ubuntu连不上网)
- Carla 开源自动驾驶仿真软件使用指南 [AD simulator]
热门文章
- php 把java list对象转成数组,java_JSON的String字符串与Java的List列表对象的相互转换,在前端:
1.如果json是List对象 - phpStudy...
- php 输出可以设置格式文件,PHP实现的文件直接输出下载
- java适配器有哪些_Java中适配器模式(Adapter)是什么? 适配器模式(详解)
- Altium Designer画元器件封装三种方法
- java复选框互斥_jmu-Java-07多线程-互斥访问 (5分)
- java hdfs导入hbase_使用BulkLoad批量导入数据到HBase中
- 输入mysql -v_Mysql数据库使用笔记
- python中常用的序列化模块_python 序列化,常用模块
- html中%3c%3e括号,打开关闭大括号检查
- 开发对接微信卡包会员卡_产品||AI刷脸会员+电子会员卡