HashMap在1.8(不含)之前对于新增元素的hash冲突的链表插入采用的是头插法,1.8之后开始改用尾插法。那么头插法有什么问题呢?为什么改用尾插法呢?源码学习一下咯

HashMap-jdk1.7.0_80

put新增map元素

public V put(K key, V value) {...// 添加新元素addEntry(hash, key, value, i);return null;
}
void addEntry(int hash, K key, V value, int bucketIndex) {// 如果当前map大小超出阈值,并且当前值再次触发了hash冲突,则resize mapif ((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);
}

resize

对map进行扩容

void resize(int newCapacity) {...Entry[] newTable = new Entry[newCapacity];// 新老table转换transfer(newTable, initHashSeedAsNeeded(newCapacity));table = newTable;threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

transfer

转换所有老table数据至新table

void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;// 1. 遍历老tablefor (Entry<K,V> e : table) {// 2. 如果元素不为空,遍历Entry元素while(null != e) {next = e.next;...int i = indexFor(e.hash, newCapacity);e.next = newTable[i];// 3. 元素放入新tablenewTable[i] = e;// 4. 继续遍历Entry子节点e = next;}}
}

扩容问题

两个线程由于并发问题,触发并发扩容

数据丢失

线程1:put c

  1. 当前old table(后面简称OT)的索引3: 3=a->b->null
  2. 当前new table(后面简称NT)的索引3:null
  3. 取出OT索引3处节点a
  4. 节点a子节点置为NT的索引3处节点,即:a->null
  5. 放入NT索引3处节点a
  6. 当前new table(后面简称NT)的索引3:3=a->null
  7. 取出OT索引3处节点的子节点b
  8. 节点b子节点置为NT的索引3处节点,即:b->a->null
  9. 放入NT索引3处节点b,并将NT索引3处的节点a置为节点b的子节点
  10. 当前new table(后面简称NT)的索引3:3=b->a->null
  11. 线程1扩容结束

线程2:put d

  1. 当前old table(后面简称OT)的索引3: 3=a->b->null
  2. 当前new table(后面简称NT)的索引3:3=null
  3. 取出OT索引3处节点a
  4. 节点c子节点置为NT的索引3处节点,即:a->null
  5. 放入NT索引3处节点a
  6. 此时节点a子节点为null,遍历结束
  7. 最终new table(后面简称NT)的索引3:3=a->null
  8. 线程2扩容结束

死循环

线程1:put c

  1. 当前old table(后面简称OT)的索引3: 3=a->b->null
  2. 当前new table(后面简称NT)的索引3:null
  3. 取出OT索引3处节点a
  4. 节点a子节点置为NT的索引3处节点,即:a->null
  5. 放入NT索引3处节点a
  6. 当前new table(后面简称NT)的索引3:3=a->null
  7. 取出OT索引3处节点的子节点b

线程2:put d

  1. 当前old table(后面简称OT)的索引3: 3=a->b->null
  2. 当前new table(后面简称NT)的索引3:3=null
  3. 取出OT索引3处节点a
  4. 节点c子节点置为NT的索引3处节点,即:a->null
  5. 放入NT索引3处节点a
  6. 此时节点a子节点为b
  7. 取出节点b,复制,NT此时为:b->a->null

线程1

  1. 节点b子节点置为NT的索引3处节点,即:b->a->null
  2. 放入NT索引3处节点b,并将NT索引3处的节点a置为节点b的子节点
  3. 当前new table(后面简称NT)的索引3:3=b->a->null
  4. 线程1扩容结束

线程2

  1. 取出节点b子节点,此时为节点a
  2. 切换指针插入NT,此时NT为:3=a->b->a,由于更新了a节点的next指针,形成环路:a->b->a
  3. 线程2扩容结束

此时两个线程或者其他读取该索引处的线程都将进入死循环,cpu也将随着死循环的增加被打满导致服务凉凉

createEntry

插入新value值计算出的索引依然为3

void createEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);size++;
}
// 创建新Entry元素
Entry(int h, K k, V v, Entry<K,V> n) {value = v;next = n;key = k;hash = h;
}

假设两个线程没有出现并发带来的扩容问题,完美完成扩容后进行插入新value值

并发插入问题

线程1:put c

  1. table索引3: 3=b->a->null
  2. 取出索引3节点:b->a->null
  3. 创建新Entry value为c

线程2:put d

  1. table索引3: 3=b->a->null
  2. 取出索引3节点:b->a->null
  3. 创建新Entry value为d
  4. 节点d子节点置为原索引3节点,即:d->b->a->null
  5. 索引3节点置为新节点,即:3=d->b->a->null

此时线程1继续执行

  1. 节点c子节点置为原索引3节点,即:c->b->a->null
  2. 索引3节点置为新节点,即:3=c->b->a->null

此时插入的节点d因并发原因丢失

HashMap-jdk1.8.0_271

put新增map元素

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}
// put value
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {...else {for (int binCount = 0; ; ++binCount) {// 遍历hash冲突处的node节点,直至为node的子节点为null,即尾节点if ((e = p.next) == null) {// 执行尾部插入p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 存在相同hash与相同key的元素,直接终止遍历重写value值即可if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}// 当存在相同hash与相同key的元素时,重写value值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;
}

并发插入问题

依然存在并发插入丢失节点问题,毕竟HashMap本身就是线程不安全

扩容问题

头插法改为尾插发解决了死循环问题

final Node<K,V>[] resize() {...Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab;if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;// 节点没有hash冲突,直接迁移至新tableif (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 红黑树结构else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);// 存在hash冲突并且非红黑树结构else { // preserve orderNode<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;// 尾插法复制do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;
}

HashMap头插法相关推荐

  1. 【Java】JDK 7 HashMap 头插法在并发情况下的成环问题

    CONTENT 问题描述 成因详解 总结 Reference 问题描述 JDK 7 的 HashMap 解决冲突用的是拉链法,在拉链的时候用的是头插,每次在链表的头部插入新元素.resize() 的时 ...

  2. 解析JDK1.7 HashMap 头插法生成的环形链表死循环问题

    背景 在JDK1.7中HashMap使用头插法来添加同一位置上的节点,但是在并发的情况下使用HashMap,在进行resize()扩容的过程中,链表可能会形成环状,当在读取HashMap元素的时候会出 ...

  3. 并发环境下hashmap头插法的问题

    hashmap的头插法问题 无意当中了解了这个问题,hashmap的数据插入的时候分为头插法和尾插法,头插法在jdk1.8之前,尾插法在jdk1.8实现. 因为头插法会出现链表成环的问题,所以插入方式 ...

  4. hashMap1.7头插法及扩容

    hashmap头插法是在jdk1.7之前版本中存在的. 一.什么是头插法 hashmap结构内部table表,当不同的元素hash相等的时候,在该位置table[i]形成链表,相同位置后来者插入到链表 ...

  5. 面试官:说说java1.7HashMap头插法导致死循环的问题

    HashMap头插法会导致死循环问题 大家天天都说Java1.7Hashmap头插法会导致死循环问题 可是大家知道为什么嘛? 下面我们来详细讲解一下全过程: 首先hashmap头插法导致死循环问题是在 ...

  6. hashmap头插法和尾插法区别_Java程序员必知:HashMap进行put操作会不会引起死循

    HashMap进行put操作会引起死循环? 最近在磕<java并发编程艺术>,在看到第六章的时候出现了下面这段我不是很理解的东西,如下 <java并发编程艺术>截取 为什么要使 ...

  7. HashMap在JDK1.7版本头插法实现解析

    HashMap在JDK1.7版本头插法实现解析 先解释下何为头插法.大家都知道HashMap在JDK1.7版本的数据结构为数组+链表这样的形式.而头插法说的就是在往HashMap里面put元素时,此时 ...

  8. hashmap头插法和尾插法区别

    前言 HashMap 应该算是 Java 后端工程师面试的必问题,因为其中的知识点太多,很适合用来考察面试者的 Java 基础. 开场 面试官: 你先自我介绍一下吧! 安琪拉: 我是安琪拉,草丛三婊之 ...

  9. HashMap面试题 头插法、尾插法、hash冲突、数组扩容、ConcurrentHashMap

    文章目录 HashMap 的数据结构? HashMap 的工作原理? HashMap 的 table 的容量如何确定?loadFactor 是什么?该容量如何变化?这种变化会带来什么问题? 数组扩容的 ...

  10. Hashmap扩容时出现循环链表(jdk1.8把头插法换成了尾插法的原因)

    参考:https://blog.csdn.net/sinat_39410753/article/details/106242573 1.容量计算 容量的阈值=容量*加载因子 2.扩容容量 扩容的容量大 ...

最新文章

  1. Powerup tinyos接口实现
  2. 卡内基梅隆大学的研究人员提出新的源定位算法: SilenceMap,寻找大脑的静默区域...
  3. 零基础学Python(第十九章 File操作·IO流补充章节)
  4. c++基础入门(根据浙大翁恺老师视频整理)
  5. iis 中后台调用exe文件
  6. JAVA(二)异常/包及访问权限/多线程/泛型
  7. hadoop安装个人心得
  8. 广东工业大学华立学院c语言试题,广东工业大学华立学院考试试卷《高频电子线路》-2015.doc...
  9. 广义预测控制及其matlab仿真,广义预测控制(GPC).doc
  10. 中学生读《皮囊》有感相关体会4300字[图]
  11. 转载 java基础题(面试必知)
  12. 投影仪与电视的C位之争,电视会成为下一个被淘汰的电器吗?
  13. 为什么滴滴裁员2000人,被裁员工却像中奖一样开心?...
  14. 视频转文字怎么操作?这三种转换方法你该学会
  15. 知道创宇创始人赵伟荣登“中国产业创新百人榜”
  16. nginx 按天分割日志
  17. vassonic PHP,轻量级、高性能的 VasSonic 框架,听说开源了?
  18. 使用正则匹配iframe标签
  19. dss linux 摄像头 rtsp,用VLC读取摄像头产生RTSP流,DSS主动取流转发(一)(二) 【转】...
  20. cscd期刊是c刊吗_武工商C刊和北大核心期刊论文发表数量位列全省同类高校前三甲...

热门文章

  1. tolua unity 报错_Unity Editor + tolua 在 Linux 下的 runtime 问题
  2. Mac OS 打开 NTFS 读写功能
  3. msdia80.dll文件出现在磁盘根目录下的解决方案(转)
  4. 微信聊天功能软件测试用例,软件测试用例实例之常见功能测试点
  5. 计算机科学与技术研究生课表,计算机科学与技术专业2018级研究生课程表.doc
  6. 装了V2.6.41,C4D启动卡死在初始化插件页面,装了RS后C4D无法启动,C4D用什么版本的RS?
  7. [转载]AutoCAD2013 以上利用AccoreConsole+ c# NetApi 批量处理图纸
  8. 知识问答题小程序头脑王者源码
  9. 1023组成最小数(20分) - 用不到20行代码破解20分的题目
  10. visio流程图怎么合并线_6步轻松做Visio跨职能流程图