HashMap头插法

article/2025/9/30 3:35:30

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

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

http://chatgpt.dhexx.cn/article/HPHYpeHV.shtml

相关文章

(最详细)c语言尾插法头插法代码讲解

1.尾插法 尾插法 头指针和尾指针都指向头结点&#xff0c;然后往里边插入元素&#xff0c; 每插入一个元素尾指针就后移一下 其中如下图所示 尾插法的核心代码是&#xff1a; pointer->next s; //pointer指向新生成的节点 pointer pointer->next;//pointer移动至新…

头插法和尾插法总结(动图版)

代码使用结构体&#xff1a; typedef struct Node{int value;struct Node* next; }*Link;头插法&#xff1a;利用头指针控制链表节点的增加。 核心&#xff1a; newNode->next head->next; head->next newNode; //头插法创建链表 Link headCreateLink(int n){//头指…

头插法和尾插法建立单链表详解与实现

写在前面&#xff1a;本文使用C语言和C引用&#xff0c;学C和C的同学都是可以看懂的&#xff0c;C毕竟向下兼容C。很详细&#xff0c;一篇能搞懂代码和原理。 先来了解几个简单概念 单链表就是线性表的链式存储&#xff1b; 头结点&#xff1a;单链表在第一个结点之前附加了一个…

链表的三种插入方法(头插法,尾插法,任意位置插入)

插入作为链表的四大基本操作之一&#xff08;增删改查&#xff09;&#xff0c;通常都会借助插入的方法增添信息&#xff0c;这一部分为大家着重讲解插入法。 1.头插法 简而言之&#xff0c;就是从链表的头部进行一个插入&#xff0c;定义一个结构体指针的新节点&#xff0c;…

【数据结构】:单链表之头插法和尾插法(动图+图解)

头插法和尾插法 一、头插法&#x1f4a4;思考一&#xff1a;头插法的核心是什么❓❗❗ 重点一&#xff1a;以带头结点方式实现头插法❗❗ 重点二&#xff1a;以不带头结点方式实现头插法 二、尾插法&#x1f4a4;思考二&#xff1a;尾插法的核心是什么❓❗❗ 重点三&#xff1a…

链表:头插法与尾插法(简易图解和代码)

头插法 定义&#xff1a;输入的数据次序生成的链表节点次序相反&#xff0c;例如&#xff1a;按1,2,3顺序进行头插之后&#xff0c;最终排序却变成了3,2,1。简而言之就是逆序插入。 定义图解&#xff1a; 代码图解&#xff1a; 代码&#xff1a;&#xff08;使用头插法建立单…

未声明的标识符

出现未声明的标识符错误&#xff0c;有可能是真的没有声明。但是也有可能是代码中有些注释&#xff0c;格式不对&#xff0c;导致声明的变量的作用域提前结束了。可以把注释都删掉试试

c2065 未声明的标识符 解决ok

场景1&#xff1a; cv::Mat pad_img; cv::Mat img; img 在一个if else后面声明&#xff0c;就报错了 解决方法&#xff0c;把cv::Mat 声明放到前面就行了&#xff0c;原因未知。 场景2&#xff1a; int a3; a为未声明的标识符&#xff0c;加了各种头文件&#xff0c;都不起…

已经包含头文件仍然出现,错误C2065“未声明的标识符”

由于当前在往一个比较大的项目中添加文件&#xff0c;文件又有相似性所以采取了复制的方式&#xff0c;最后出现了一个大疏漏。 在总的.cpp文件中调用新文件中的函数&#xff0c;在包含了新文件的.h头文件的情况下仍然说没有找到标识符&#xff0c;在网上找了很多方法&#xff…

c++: “default”: 未声明的标识符

c&#xff1a; “default”: 未声明的标识符 c&#xff1a; “default”: 未声明的标识符 1、错误描述 2、错误原因&#xff1a; 3、解决方案&#xff1a; 1、错误描述 错误 C2065 “default”: 未声明的标识符 2、错误原因&#xff1a; C11 标准的新特性…

C++命名空间namspace解析——“cout”未声明的标识符,“cin”未声明的标识符

首先我们先看一下下面这段代码运行时的情况&#xff08;注意按ctrlF5 运行&#xff09; #include<iostream> int main() {return 0; }运行结果如下 是一个没有任何结果的窗口 现在我们再加上一段输出代码 cout<<"hello"<<endl;会发现编译运行时…

error C2065: “string”: 未声明的标识符//error C2065: “vector”: 未声明的标识符

添加头文件没&#xff0c; #include <stdio.h> #include <string.h> #include #include using std::vector; #include <stdio.h> #include <string.h> #include <iostream> #include <vector> using std::vector;就这样&#xff0c;先&…

使用控件时提示未声明标识符的解决方法

环境&#xff1a;VS2012&#xff0c;Win8.1 64bit 参考&#xff1a; http://www.cnblogs.com/Romi/archive/2012/01/06/2314390.html http://zhidao.baidu.com/question/256304139.html 1. 用MFC默认创建Dialog类型项目&#xff0c;里面有个VS默认添加的CStatic控件 2. 如果…

引入头文件结构体,解决未声明标识符fileinfo

首先我问大佬&#xff0c;大佬让我右键单击结构体_finddata_t,找下它的定义&#xff0c;但是我没找到啥。然后大佬说让我引入结构体所在的头文件。我就右键单击_finddata_t&#xff0c;联机搜索&#xff0c;找到了它所在的头文件io.h&#xff0c;成功解决了未声明标识符fileinf…

未声明标识符怎么解决oracle,请问 什么是“未声明的标识符”错误,如何解决?...

月关宝盒 它们通常来自忘记包含包含函数声明的头文件&#xff0c;例如&#xff0c;此程序将给出“未声明的标识符”错误&#xff1a;缺少标题int main() { std::cout < return 0;}要修复它&#xff0c;我们必须包含标题&#xff1a;#include int main() { std::cout < re…

vs编译运行报错:未声明的标识符

报错如下图&#xff1a; 很奇怪&#xff0c;我这个testList是已经声明的&#xff0c;就在上一行&#xff0c;但是还是会给我报错&#xff1a;未声明的标识符。 最后发现原因是在报warning的地方。 解决办法&#xff1a; 在代码处右击出现菜单栏&#xff0c;点击最下方的【保存…

未声明标识符怎么解决oracle,什么是“未声明的标识符”错误,如何解决?

潇潇雨雨 它们通常来自忘记包含包含函数声明的头文件&#xff0c;例如&#xff0c;此程序将给出“未声明的标识符”错误&#xff1a;缺少标题int main() { std::cout < return 0;}要修复它&#xff0c;我们必须包含标题&#xff1a;#include int main() { std::cout < re…

插值算法的介绍及其在数学建模中的应用

目录 插值算法的介绍及其在数学建模中的应用 一、插值的介绍及其作用 二、插值法原理 三、插值法的分类 1、普通多项式插值 2、分段低次插值 3、&#xff08;三次&#xff09;样条插值 4、分段三次埃尔米特&#xff08;Hermite&#xff09;插值 插值算法的介绍及其在数…

插值方法学习

0.摘要 我感觉上采样阶段要放在特征提取的前期&#xff0c;而不是后期&#xff0c;因为后期的feature map太小了&#xff0c;而且相邻间的像素值会存在突变&#xff0c;会造成增加的噪声概率会比较高。参考图像插值技术综述学习了一下插值方法 1.单线性插值法 已知ac&#xf…

插值查找算法

插值查找算法 插值查找算法又称插值搜索算法&#xff0c;是在二分查找算法的基础上改进得到的一种查找算法。 插值查找算法只适用于有序序列&#xff0c;换句话说&#xff0c;它只能在升序序列或者降序序列中查找目标元素。作为“改进版”的二分查找算法&#xff0c;当有序序…