点击上方“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 为什么线程不安全?相关推荐

  1. java类加载过程_面试官:java类的加载过程

    Java 类加载机制 类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段. 其中类加载过程包括加载.验证.准备.解析和初始化五个阶段. 类的加载 ...

  2. java进程内存一直没释放_面试官:一个线程OOM,进程里其他线程还能运行么?...

    引言 这题是一个网友@大脸猫爱吃鱼给我的提问,出自今年校招美团三面的一个真题.大致如下 一个进程有3个线程,如果一个线程抛出oom,其他两个线程还能运行么? 先说一下答案,答案是还能运行 不瞒大家说, ...

  3. redis怎么修改_面试官问我Redis事务,还问我有哪些实现方式

    ❝ 「第12期」 距离大叔的80期小目标还有68期,今天大叔要跟大家分享的内容是 -- Reids中的事务.同样,这也是redis中重要指数为四颗星的必备基础知识点.下面一起来了解一下吧. ❞ 相信大 ...

  4. java 面试题 由浅入深_面试官由浅入深的面试套路

    阅读文本大概需要3分钟. 从上图看来面试官面试是有套路的,一不小心就一直被套路. 0x01:Thread 面试官 :创建线程有哪几种方式? 应聘者 :继承Thread类.实现Runable接口.使用j ...

  5. hashmap put过程_阿里面试官:HashMap数据结构之道

    问题1:HashMap的数据结构是什么样的? 同学1:嗯...数组+链表 同学2:数组+链表... 同学3:数组+链表... 同学4:数组+链表+红黑树... 同学n:..... 为什么答案会有两种? ...

  6. 哈希表查找失败的平均查找长度_面试官:哈希表都不知道,你是怎么看懂HashMap的?...

    本文作者 作者:马可没有菠萝 链接: https://juejin.im/post/6876105622274703368 本文由作者授权发布. HashMap是Java面试中的必问考点之一,网上关于 ...

  7. hashmap扩容_面试官问:HashMap在并发情况下为什么造成死循环?一脸懵

    这个问题是在面试时常问的几个问题,一般在问这个问题之前会问Hashmap和HashTable的区别?面试者一般会回答:hashtable是线程安全的,hashmap是线程不安全的. 那么面试官就会紧接 ...

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

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

  9. hashmap put过程_看完还不懂HashMap算我输(附互联网大厂面试常见问题)

    HashMap的原理与实现 版本之更迭: –>JDK 1.7 : Table数组+ Entry链表: –>JDK1.8 : Table数组+ Entry链表/红黑树:(为什么要使用红黑树? ...

最新文章

  1. 仙道服务器维护,5月22日8:00全区停机维护 刺金传说上线
  2. opencv---JPEG图像质量检测代码
  3. mac版python连接mysql_Mac下Python连接MySQL · BlBana’s BlackHouse
  4. python qt教程_Python - Python Qt 开发教程(1)
  5. 科学小世界,婚姻大殿堂
  6. python openstack rabbitmq_OpenStack--Rabbitmq组件消息队列
  7. 通过Ajax方式上传文件(input file),使用FormData进行Ajax请求
  8. kafka分区与分组原理_大数据技术-Kafka入门
  9. SPRING IN ACTION 第4版笔记-第二章-001-用@Autowired\@ComponentScan、@Configuration、@Component实现自动装载bean...
  10. angular 注入器配置_注入器和发布库–AngularJS学习笔记(三)
  11. 掌阅科技前三季度净利润1.66亿元 同比增长53.75%
  12. spring中context:property-placeholder
  13. 使用Hadoop搭建现代电信企业架构
  14. 小米蓝牙音响驱动_拆解报告:小米无线充蓝牙音箱
  15. 文章目录---收藏不迷路
  16. android自定义速度仪表盘,自定义View实战:汽车速度仪表盘
  17. 3.抽象类:什么是抽象类???抽象类的特点有哪些???
  18. 三菱FX5U位逻辑指令
  19. ubuntu出现有线已连接却无法上网的解决方法(ubuntu连不上网)
  20. Carla 开源自动驾驶仿真软件使用指南 [AD simulator]

热门文章

  1. php 把java list对象转成数组,java_JSON的String字符串与Java的List列表对象的相互转换,在前端: 1.如果json是List对象 - phpStudy...
  2. php 输出可以设置格式文件,PHP实现的文件直接输出下载
  3. java适配器有哪些_Java中适配器模式(Adapter)是什么? 适配器模式(详解)
  4. Altium Designer画元器件封装三种方法
  5. java复选框互斥_jmu-Java-07多线程-互斥访问 (5分)
  6. java hdfs导入hbase_使用BulkLoad批量导入数据到HBase中
  7. 输入mysql -v_Mysql数据库使用笔记
  8. python中常用的序列化模块_python 序列化,常用模块
  9. html中%3c%3e括号,打开关闭大括号检查
  10. 开发对接微信卡包会员卡_产品||AI刷脸会员+电子会员卡