大厂面试爱问的HashMap死锁问题,看这一篇就够了

  • JDK 1.7 HashMap源码分析
    • put()方法
    • addEntry()方法
    • resize()方法
    • transfer()方法(重点)
  • 死锁演示
  • 如何规避
    • 使用Hashtable 或 ConcurrentHashMap
    • JDK1.8的升级和仍存在的死锁问题
      • 升级内容
      • 仍可能存在死锁问题

经历过大厂面试或者有所了解的同学都应该知道,HashMap是面试时面试官特别喜欢的问题,除了HashMap的扩容方式,为什么扩容的2的次幂等以外,还经常会问到HashMap死锁的相关问题。最常出现的死锁问题的是在JDK 1.7版本,为了理解死锁问题产生的原因我们来从源码和一些相关概念开始说起。

JDK 1.7 HashMap源码分析

put()方法

public V put(K key, V value) {if (table == EMPTY_TABLE) {inflateTable(threshold);}if (key == null)return putForNullKey(value);int hash = hash(key);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++;addEntry(hash, key, value, i);return null;}

put()方法可以总结成以下四个过程:

  1. 特殊 key 值处理,key 为 null;(在JDK 1.7版本 key可以为null,如果是第一次put会被用头插法存在bucket[0]的位置,在JDK1.8以后则会直接报异常)

  2. 计算 table 中目标 bucket 的下标;

    int i = indexFor(hash, table.length);
    indexFor的源码如下:/** * Returns index for hash code h. */static int indexFor(int h, int length) {return h & (length-1);}
    

实际上是目标hash值和bucket和bucket长度-1,也就是length-1进行“与”运算,当bucket的长度只能是2的次幂的的时候,其实也就相当于目标值对bucket长度进行取余运算,只不过这样效率更高
3. 指定目标 bucket,遍历 Entry 结点链表,若找到 key 相同的 Entry 结点,则做替换;
4. 若未找到目标 Entry 结点,则新增一个 Entry 结点

大家可能不太懂的一个操作是modCount,它是一个记录 map 新增/删除 k-v 对次数的变量。它的主要作用,是对 Map 的iterator()操作做一致性校验,如果在 iterator 遍历操作的过程中,map 的数值有变化,直接抛出ConcurrentModificationException异常。

addEntry()方法

void addEntry(int hash, K key, V value, int bucketIndex) {if ((size >= threshold) && (null != table[bucketIndex])) {resize(2 * table.length);hash = (null != key) ? hash(key) : 0;bucketIndex = indexFor(hash, table.length);}createEntry(hash, key, value, bucketIndex);}
  1. 查看当前的size是否超过了我们设定的阈值threshold,如果超过且当前的 bucket 下标有链表存在,需要resize()
  2. bucket扩容为原来两倍,重新计算hash存入不同的bucket
  3. 采用头插法新增节点

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];transfer(newTable, initHashSeedAsNeeded(newCapacity));table = newTable;
}
  1. bucket扩容为原来的两倍
  2. 重新计算hash,数据迁移

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;}}
}
  1. 对链表上的每一个节点遍历,当前节点e不为空时,e的next 指向e在当前bucket下的后一个(e指向的是没有转移的时候的下一个)
  2. 重新计算e的hash,计算新的对应的bucket下标
  3. 先将 e.next 指向新 Hash 表的第一个元素,newTable[i]上的值赋给e元素的next属性,e属性再赋值给newTable[i],这样newTable[i]上的链表新的元素都会靠前,之前的元素相当于后移了
    (假如转移前链表顺序是1->2->3,那么转移后就会变成3->2->1)

死锁演示

我们以链表a->b->c->null为例,两个线程 A 和 B,分别做扩容操作。
原表:

A 和 B 各自新增了一个新的哈希 table,在线程 A 已做完扩容操作后,线程 B 才开始扩容。此时对于线程 B 来说,当前结点e指向 a 结点,下一个结点e.next仍然指向 b 结点(此时在线程 A 的链表中,已经是c->b->a的顺序)。按照头插法,哈希表的 bucket 指向 a 结点,此时 a 结点成为线程 B 中链表的头结点,如下图所示:

a 结点成为线程 B 中链表的头结点后,下一个结点e.next为 b 结点。既然下一个结点e.next不为 null,那么当前结点e就变成了 b 结点,下一个结点e.next变为 a 结点。继续执行头插法,将 b 变为链表的头结点,同时 next 指针指向旧的头节点 a,如下图:

此时,下一个结点e.next为 a 节点,不为 null,继续头插法。指针后移,那么当前结点e就成为了 a 结点,下一个结点为 null。将 a 结点作为线程 B 链表中的头结点,并将 next 指针指向原来的旧头结点 b,如下图所示:
此时,已形成环链表。同时下一个结点e.next为 null,流程结束。

图片引用:https://blog.csdn.net/valada/article/details/103359320?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159811189919195264506720%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=159811189919195264506720&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v3~pc_rank_v4-2-103359320.first_rank_ecpm_v3_pc_rank_v4&utm_term=hashmap面试就够了&spm=1018.2118.3001.4187

如何规避

使用Hashtable 或 ConcurrentHashMap

Hashtable 或 ConcurrentHashMap都是线程安全的,不过Hashtable效率低下,ConcurrentHashMap采用了分段锁,效率更高

JDK1.8的升级和仍存在的死锁问题

升级内容

  1. 由数组+链表的结构改为数组+链表+红黑树。

  2. 扩容方法从头插法改成了尾插法,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变

仍可能存在死锁问题

  1. 多线程put的时候可能导致元素丢失

  2. put非null元素后get出来的却是null

大厂面试爱问的HashMap死锁问题,看这一篇就够了相关推荐

  1. HashMap面试,看这一篇就够了

    历史热门文章: 七种方式教你在SpringBoot初始化时搞点事情 Java序列化的这三个坑千万要小心 可以和面试官聊半个小时的volatile原理 Java中七个潜在的内存泄露风险,你知道几个? J ...

  2. 面试被问到 ConcurrentHashMap答不出 ,看这一篇就够了!

    本文汇总了常考的 ConcurrentHashMap 面试题,面试 ConcurrentHashMap,看这一篇就够了!为帮助大家高效复习,专门用"★ "表示面试中出现的频率,&q ...

  3. redis hashmap过期_看完这篇再也不怕 Redis 面试了

    0.前言 Redis是跨语言的共同技术点,无论是Java还是C++都会问到,所以是个高频面试点. 笔者是2017年才开始接触Redis的,期间自己搭过单机版和集群版,不过现在 大一些的 公司都完全是运 ...

  4. HashMap面试题,看这一篇就够了!

    在程序员这一职业中,集合是我们使用频率相当高的一个工具,而其中的 HashMap,则更是我们用以处理业务逻辑的好帮手,同时 HashMap 的底层实现和原理,也成了面试题中的常客. 还在担心面试中被问 ...

  5. mysql映射成hashmap_大厂面试必问!HashMap 怎样解决hash冲突?

    HashMap冲突解决方法比较考验一个开发者解决问题的能力. 下文给出HashMap冲突的解决方法以及原理分析,无论是在面试问答或者实际使用中,应该都会有所帮助. 在Java编程语言中,最基本的结构就 ...

  6. hashmap存多少条数据_干货 | 面试官想问的HashMap,都在这一篇里面了!

    来源公众号:非科班的科班 本文思维导图 HashMap简介 HashMap 是很常用的一种集合框架,其底层实现方式在 JDK 1.7和 JDK 1.8中却有很大区别.HashMap 是用来存储数据的, ...

  7. HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!

    前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据. 本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 ...

  8. JVM 面试都问些啥?看这一篇就够了

    昨晚,我在路口等车的时候,听到几个人在那讨论问题: "之前我用 jprofiler 监控 jvm 里的对象,当老年代满了,我手动触发一次 fgc,发现只能回收一半,再触发一次,就完全回收,这 ...

  9. 安卓开发培训!没想到一个Handler还有中高级几种问法,看这一篇就够了!

    前言 随着移动网络的不断升级,客户端的网络传输由3G进化到Wifi.4G.5G,且Wifi场景越来越多.虽然网络环境在变好,但也对网络的应用提出了更高的要求,会发现很多大厂都十分重视网络指标,如果技术 ...

最新文章

  1. ckeditor4.4.6添加代码高亮
  2. 为什么 StringBuilder 不是线程安全的?
  3. JavaScript数组方法大全(推荐)
  4. 禁止ping入自己的主机
  5. 微软奇迹之旅-----天津站
  6. 为什么要用dubbo,dubbo和zookeeper关系
  7. java BigDecimal八种舍入模式
  8. [渝粤教育] 西南科技大学 中学英语教材教法 在线考试复习资料
  9. JS中与正则相关的方法
  10. java获取类的信息
  11. 蔺永华:虚拟化你的大数据应用
  12. java 静态类实例_Java中多个类的静态实例?
  13. vue 左右循环滑动_vue实现无缝滚动循环
  14. linq 连接mysql_如何:使用 LINQ 查询数据库 - Visual Basic | Microsoft Docs
  15. APP开发短信接口集成
  16. 用matlab制作证件照,美图秀秀证件照制作方法图文教程
  17. Basic 语言发展史
  18. 跑马灯的一些使用心得
  19. vue中views新建文件夹的代码规范
  20. mapreduce新编程实例

热门文章

  1. 移动硬盘变成RAW格式的恢复
  2. hbase+dataframe+java_Java实现Spark将DataFrame写入到HBase
  3. Codeforces 1244F Chips
  4. JSP cookie详解
  5. 软件架构设计师:用户界面设计的原则
  6. NULL与nullptr
  7. 16 | 网络优化(中):复杂多变的移动网络该如何优化?
  8. win7文件权限设置
  9. linux网卡混杂模式和监听模式
  10. DirectX12初始化一——DX::ThrowIfFailed使用