深入理解hashmap底层实现原理

目录

总体介绍

HashMap元素的存储

在hashmap中添加元素

HashMap的扩容机制

HashMap的线程安全性

1.添加和删除元素时存在不安全性

2.进行扩容操作时存在不安全性

3.哈希冲突存在不安全性

4.线程之间的不可见性导致安全问题


总体介绍

HashMap是我们用于元素映射使用频率最高的数据结构,它继承自AbstractList类,并且支持一条值为null的Key和无数条value为null的数据,HashMap是线程不安全的6在多线程环境下我们通过使用Collections中的synchronizedMap使其具有线程安全的能力或者直接使ConcurrentHashMap,随着JDK的更新迭代,自jdk1.8以来,HashMap的底层数据结构已经发展为数组+链表+红黑树

HashMap元素的存储

HashMap底层使用Node<K,V>进行元素的存储,我们查看其源码:

static class Node<K,V> implements Map.Entry<K,V> {final int hash;    //用来定位数组索引位置final K key;V value;Node<K,V> next;   //链表的下一个nodeNode(int hash, K key, V value, Node<K,V> next) { ... }public final K getKey(){ ... }public final V getValue() { ... }public final String toString() { ... }public final int hashCode() { ... }public final V setValue(V newValue) { ... }public final boolean equals(Object o) { ... }
}

HashMap的源码中,一个比较重要的组成部分是Node[]table,即哈希桶数组,元素根据哈希算法最终被放入哈希桶不同下标的位置中,随着元素的不断增多,可能存在两个不同的元素有相同的下标,这便是哈希冲突,哈希桶解决哈希冲突的方法是链式地址法,所谓链式地址法,是指通过采用链表+数组的方式解决哈希冲突问题,随着哈希桶内元素的不断增加,当单个链表元素数量大于等于8以及哈希桶中的总元素数量>=64时,链表就会转化为红黑树

在hashmap中添加元素

在HashMap中添加元素的逻辑如下:

1.判断数组table[i]是否为空,如果是空,则执行resize()进行扩容

2.根据键值key计算hash值得到应该插入的数组索引i,如果table[i]==null,直接创建新结点并加入数组,转向6;如果table[i]对应的元素不为空,转向3

3.判断table[i]首个元素是否与我们插入的元素完全相同(根据hashcode()以及equals()进行判断),如果相同直接覆盖value,否则转向4

4.判断table[i]是否是treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在红黑树中插入键值对,否则转向5

5.遍历table[i],判断链表的长度是否大于等于8,如果满足条件直接把链表转化为红黑树,否则在链表中插入元素,遍历链表的过程中如果发现相同的key直接覆盖value即可

6.扩容成功后,判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过直接进行扩容

HashMap中的源码如下:

1 public V put(K key, V value) {
 2     // 对key的hashCode()做hash
 3     return putVal(hash(key), key, value, false, true);
 4 }
 5 
 6 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 7                boolean evict) {
 8     Node<K,V>[] tab; Node<K,V> p; int n, i;
 9     // 步骤①:tab为空则创建
10     if ((tab = table) == null || (n = tab.length) == 0)
11         n = (tab = resize()).length;
12     // 步骤②:计算index,并对null做处理 
13     if ((p = tab[i = (n - 1) & hash]) == null) 
14         tab[i] = newNode(hash, key, value, null);
15     else {
16         Node<K,V> e; K k;
17         // 步骤③:节点key存在,直接覆盖value
18         if (p.hash == hash &&
19             ((k = p.key) == key || (key != null && key.equals(k))))
20             e = p;
21         // 步骤④:判断该链为红黑树
22         else if (p instanceof TreeNode)
23             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
24         // 步骤⑤:该链为链表
25         else {
26             for (int binCount = 0; ; ++binCount) {
27                 if ((e = p.next) == null) {
28                     p.next = newNode(hash, key,value,null);
                        //链表长度大于8转换为红黑树进行处理
29                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  
30                         treeifyBin(tab, hash);
31                     break;
32                 }
                    // key已经存在直接覆盖value
33                 if (e.hash == hash &&
34                     ((k = e.key) == key || (key != null && key.equals(k))))                                          break;
36                 p = e;
37             }
38         }
39         
40         if (e != null) { // existing mapping for key
41             V oldValue = e.value;
42             if (!onlyIfAbsent || oldValue == null)
43                 e.value = value;
44             afterNodeAccess(e);
45             return oldValue;
46         }
47     }

48     ++modCount;
49     // 步骤⑥:超过最大容量 就扩容
50     if (++size > threshold)
51         resize();
52     afterNodeInsertion(evict);
53     return null;
54 }

HashMap的扩容机制

与JDK 1.7相比,JDK1.8在原来扩容的基础上添加了红黑树,其实现的流程和条件如下:当满足扩容条件时,进行扩容操作(哈希表中的元素个数达到threshold),进行扩容操作时如果发现满足树化的条件(单条链表元素数量>=8&&哈希桶中存储的总元素的数量>=64)则将链表转化为红黑树。

具体的源码及分析如下:

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) {if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;//超过了哈希表的最大容量不进行扩容,任由其碰撞}else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)//数组容量扩大为两倍newThr = oldThr << 1; // 将临界值扩大为两倍}else if (oldThr > 0) // initial capacity was placed in thresholdnewCap = oldThr;else {               // zero initial threshold signifies using defaultsnewCap = DEFAULT_INITIAL_CAPACITY;//初始化容量newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//初始化扩容门槛}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;@SuppressWarnings({"rawtypes","unchecked"})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;if (e.next == null)newTab[e.hash & (newCap - 1)] = e;else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);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 {//新的索引=原索引+oldCapif (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);//原索引放入bucketif (loTail != null) {loTail.next = null;newTab[j] = loHead;}//新索引放入bucketif (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

除此之外,我们通过查看resize()的源码,我们可以发现JDK1.8对扩容之后元素的索引变化也是有一定的优化:经过观测可以发现,我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。

元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:

因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图

 这个设计可以说是相当巧妙了,既省略了类似JDK1.7的扩容之后重新计算hash的这样一个过程,提升了效率;除此之外,由于我们新增的一位是1还是0完全是随机的,所以可以把之前存在哈希冲突的结点有效的均匀分散到新的bucket中了。

HashMap的线程安全性

hashmap的线程不安全性主要体现在下面四个方面:

1.添加和删除元素时存在不安全性

在并发环境下,如果多个线程同时对HashMap进行修改操作,例如:添加、删除或修改元素,可能会导致数据结构出现冲突,最终可能会导致数据不一致或丢失。

这是因为多个线程在同一时间可能会尝试修改同一个桶(bucket)中的数据,而在修改过程中,可能会丢失某些数据或者覆盖其他线程修改的数据,从而导致数据不一致。

2.进行扩容操作时存在不安全性

HashMap 在达到一定容量时需要进行扩容,扩容过程需要重新计算哈希值,重新分配存储位置等操作。如果在扩容过程中有多个线程同时进行插入或删除操作,就可能导致数据结构混乱,甚至死循环等问题。

3.哈希冲突存在不安全性

在对数组进行插入或删除操作时,可能会出现 Hash 冲突,即不同的键值对可能会被映射到数组的同一个位置上,这就需要通过链表或红黑树来解决冲突。

而当多个线程同时进行插入或删除操作时,可能会导致链表或红黑树的结构被破坏,从而导致数据丢失或出现异常。

4.线程之间的不可见性导致安全问题

由于 HashMap 不是线程安全的,因此多个线程可能会同时访问同一个 HashMap 实例。

而当一个线程对 HashMap 进行修改时,其他线程可能无法立即看到这些修改,这就可能导致数据不一致的问题。

为了解决 HashMap 的线程安全问题,可以使用 ConcurrentHashMap,它使用了锁分段技术、CAS、 volatile 变量来保证线程安全和数据可见性。

但是,这些技术也会带来一些额外的开销,所以在非高并发场景下,HashMap可能会比ConcurrentHashMap更快。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/142795.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

RK3588平台开发系列讲解(项目篇)YOLOv5部署测试

平台内核版本安卓版本RK3588Linux 5.10Android 12文章目录 一、YOLOv5环境安装二、YOLOv5简单使用2.1、获取预训练权重文2.2、YOLOv5简单测试2.3、转换为rknn模型2.4、部署到 RK 板卡三、airockchip/yolov5简单测试3.1、转换成rknn模型并部署到板卡沉淀、分享、成长,让自己和他…

HTML编码问题导致的乱码

今天一个学弟问了一个问题&#xff0c;它写的HTML代码打开显示的是乱码。 然后就给我发来了代码。 就一个HTML文件和一个文件夹&#xff0c;打开一看&#xff0c;很简单的代码。 <!DOCTYPE html> <html lang""> <head><meta charset"UTF…

URL和HTML编码

URL和HTML编码 在呈现HTML页面时,有时候需要显示一些特殊的字符,例如”<”和”&”,因为它们是HTML专用字符,因此需要一些技巧.例如要想显示AT&T,在代码中必须写成AT&amp;T。同样URL中的,&,/等字符也为专用字符,所以如果需要在URL参数中使用它们,也必须对这些…

简单的Html编码转换工具

一、前言 因为项目经常会碰Html转码&#xff0c;特别是返回的xml报文&#xff0c;总是显示&# 十六进制的编码&#xff0c;查中文的时候特别不方便&#xff0c;所以就做了一个简单的C#桌面应用程序。 二、涉及软件 Visual Studio 2019 Preview 三、使用框架 .NET Fram…

怎么设置html代码中的编码格式,html怎么设置编码

在html中&#xff0c;可以使用meta标签来设置编码&#xff0c;语法格式“”。meta标签提供了HTML文档的元数据&#xff0c;元数据不会显示在客户端&#xff0c;但是会被浏览器解析&#xff1b;而charset属性用于定义文档的字符编码。 本教程操作环境&#xff1a;windows7系统、…

Unicode编码对应的HTML 、JS 、CSS码

平时写代码很少用到HTML的特殊字符&#xff0c;最常用的可能是 了&#xff0c;但有时在移动端为了节省时间&#xff0c;可能会用这些字符实现某种特殊效果&#xff0c;现整理如下&#xff1a; 使用方法&#xff1a; 这些字符属于unicode字符集&#xff0c;所以&#xff0c;你…

HTML之编码规范

HTML之编码规范 img 标签要写 alt 属性单标签不要写闭合标签自定义属性要以 data-开头td 要在 tr 里面&#xff0c;li 要在 ul/ol 里面ul/ol 的直接子元素只能是 lisection 里面要有标题标签使用 section 标签增强 SEO行内元素里面不可使用块级元素每个页面要写要用 table 布局…

【HTML 教程】HTML 字符编码

作者 | 阮一峰 简介 网页包含了大量的文字&#xff0c;浏览器必须知道这些文字的编码方法&#xff0c;才能把文字还原出来。 一般情况下&#xff0c;服务器向浏览器发送 HTML 网页文件时&#xff0c;会通过 HTTP 头信息&#xff0c;声明网页的编码方式。 Content-Type: text/ht…

HTML编码规范

本篇文章是基于王叨叨大佬师父维护的文档梳理的&#xff0c;有兴趣可以去看一下原文HTML编码规范。 1. 缩进与换行 【建议】 使用 2 个空格作为一个缩进层级&#xff0c;不允许使用tab字符 解释&#xff1a; ​ 具体项目&#xff0c;可以使用2个空格&#xff0c;也可以使用…

HTML 编码(字符集)总结,你了解了多少

Web 浏览器必须知道要使用哪个字符集&#xff0c;才能正确显示 HTML 页面。 文章目录 Web 浏览器必须知道要使用哪个字符集&#xff0c;才能正确显示 HTML 页面。前言一、HTML charset 属性二、字符集之间的差异ASCII 字符集ANSI 字符集 (Windows-1252)ISO-8859-1 字符集UTF-8 …

【HTML基础笔记】之【常用编码】

HTML 常用编码 4.1 HTML 实体编码 HTML实体编码&#xff0c;也即HTML中的转义字符。 在 HTML 中&#xff0c;某些字符是预留的&#xff0c;例如在 HTML 中不能使用小于号<和大于号>&#xff0c;这是因为浏览器会误认为它们是标签。如果希望正确地显示预留字符&#xf…

CTR预估之DNN系列模型:FNN/PNN/DeepCrossing

前言 在上一篇文章中 CTR预估之FMs系列模型:FM/FFM/FwFM/FEFM&#xff0c;介绍了FMs系列模型的发展过程&#xff0c;开启了CTR预估系列篇章的学习。FMs模型是由线性项和二阶交互特征组成&#xff0c;虽然有自动学习二阶特征组合的能力&#xff0c;一定程度上避免了人工组合特征…

SQL SERVER DATEPART函数

定义&#xff1a; DATEPART函数返回指定日期的指定部分。 语法&#xff1a; DATEPART(datepart,date) 参数&#xff1a; ①datepart 参数可以是下列的值&#xff1a; datepart缩写年(Year)YEAR, YY, YYYY季度(Quarter)Q, QQ, QUARTER月(Month)M, MM, MONTH年中的日(Day of year…

DATEPART SQL函数

This article explores the DATEPART SQL function and its use in writing t-SQL queries. In the previous article, SQL Convert Date Functions and Formats, we explored various data formats and convert them using SQL Convert function. 本文探讨了DATEPART SQL函数…

mysql datepart_SQL Server DATEPART() 函数用法

用法&#xff1a; DATEPART() 函数用于返回日期/时间的单独部分&#xff0c;比如年、月、日、小时、分钟等等。返回的类型为整型。若要返回字符型可以用DATENAME()函数&#xff0c;可用于时间日期之间的拼接&#xff0c;用法和DATEPART()类似 语法&#xff1a; DATEPART(datepa…

mysql datepart_表达式中datepart函数用法及其与sqlserver depart函数、Mysql week函数的差异...

Wyn Reports支持丰富的函数&#xff0c;这些函数是实现各种计算需求的表达式的基础。 DatePart函数一个日期类函数&#xff0c;返回一个 Integer 值&#xff0c;其中包含给定 Date 值的指定部分(年&#xff0c;月&#xff0c;日&#xff0c;时&#xff0c;分&#xff0c;秒&…

DatePart 函数

DatePart 函数 适用于: Microsoft Office Access 2007 全部显示 全部隐藏 返回变量型&#xff08;整型&#xff09;&#xff0c;其中包含给定日期的指定部分。 语法 DatePart(interval, date [, firstdayofweek] [, firstweekofyear] ) DatePart 函数的语法包含以下参数 &…

sql学习---datepart函数的使用

DATEPART 返回代表指定日期的指定日期部分的整数。 语法 DATEPART ( datepart ,date ) 参数 datepart 是指定应返回的日期部分的参数。下表列出了 Microsoft SQL Server™ 识别的日期部分和缩写。 如图&#xff1a; 另外补充&#xff1a; 当 group by datepart (yyyy,date), da…

datepart函数用法及其与sqlserver depart函数、Mysql week函数的差异

Wyn Reports支持丰富的函数&#xff0c;这些函数是实现各种计算需求的表达式的基础。DatePart函数一个日期类函数&#xff0c;返回一个 Integer 值&#xff0c;其中包含给定 Date 值的指定部分&#xff08;年&#xff0c;月&#xff0c;日&#xff0c;时&#xff0c;分&#xf…

sqlserver 截取日期年份和月份使用datepart函数

sqlserver 截取日期年份和月份使用datepart函数,函数使用方法如下&#xff1a; 一、函数功能&#xff1a;DATEPART() 函数用于返回日期/时间的单独部分&#xff0c;比如年、月、日、小时、分钟等等。 二、语法&#xff1a;DATEPART(datepart,date) 三、参数说明&#xff1a;…