hashmap 存取原理图_HashMap底层实现原理

HashMap底层原理总结,几个Hash集合之间的对比。

HashMap底层存储结构

HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做一个Entry。这些Entry分散存储在一个数组当中,这个数组就是HashMap的主干。1

2

3

4

5

6

7* The table, initialized on first use, and resized as

* necessary. When allocated, length is always a power of two.

* (We also tolerate length zero in some operations to allow

* bootstrapping mechanics that are currently not needed.)

*/

transient Node[] table;1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18* Basic hash bin node, used for most entries. (See below for

* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)

*/

static class implements Map.Entry{

final int hash;

final K key;

V value;

Node next;

Node(int hash, K key, V value, Node next) { ... }

public final K getKey(){ return key; }

public final V getValue(){ return value; }

public final String toString(){ return key + "=" + value; }

public final int hashCode(){ return Objects.hashCode(key) ^ Objects.hashCode(value);}

public final V setValue(V newValue){ ... }

public final boolean equals(Object o){ ... }

}

因为table数组的长度是有限的,再好的hash函数也会出现index冲突的情况,所以我们用链表来解决这个问题,table数组的每一个元素不只是一个Entry对象,也是一个链表的头节点,每一个Entry对象通过Next指针指向下一个Entry节点。当新来的Entry映射到冲突数组位置时,只需要插入对应的链表即可。

需要注意的是:新来的Entry节点插入链表时,会插在链表的头部,因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。

HashMap中的table数组如下所示:

所以,HashMap是数组+链表+红黑树(在Java 8中为了优化Entry的查找性能,新加了红黑树部分)实现的。

Put方法原理

调用hashMap.put("str", 1),将会在HashMap的table数组中插入一个Key为“str”的元素,这时候需要我们用一个hash()函数来确定Entry的插入位置,而每种数据类型有自己的hashCode()函数,比如String类型的hashCode()函数如下所示:1

2

3

4

5

6

7public static int hashCode(byte[] value){

int h = 0;

for (byte v : value) {

h = 31 * h + (v & 0xff);

}

return h;

}

所以,put()函数的执行路径是这样的:首先put("str", 1)会调用HashMap的hash("str")方法。

在hash()内部,会调用String(Latin1)内部的hashcode()获取字符串”str”的hashcode。

“str”的hashcode被返回给put(),put()通过一定计算得到最终的插入位置index。

最后将这个Entry插入到table的index位置。

这里就出现了两个问题,问题1: 在put()里怎样得到插入位置index?问题2: 为什么会调用HashMap的hash()函数,直接调用String的hashcode()不好吗?

问题1: 在put()里怎样得到插入位置index?

对于不同的hash码我们希望它被插入到不同的位置,所以我们首先会想到对数组长度的取模运算,但是由于取模运算的效率很低,所以HashMap的发明者用位运算替代了取模运算。

在put()里是通过如下的语句得到插入位置的:1index = hash(key) & (Length - 1)

其中Length是table数组的长度。为了实现和取模运算相同的功能,这里要求(Length - 1)这部分的二进制表示全为1,我们用HashMap的默认初始长度16举例说明:1

2

3

4

5假设"str"的hash吗为: 1001 0110 1011 1110 1101 0010 1001 0101

Length - 1 = 15 : 1111

hash("str") & (Length - 1) = 0101

如果(Length - 1)这部分不全为1,假如Length是10,那么Length - 1 = 9 :1001 那么无论再和任何hash码做与操作,中间两位数都会是0,这样就会出现大量不同的hash码被映射到相同位置的情况。

所以,在HashMap中table数组的默认长度是16,并且要求每次自动扩容或者手动扩容时,长度都必须是2的幂。

问题2: 为什么会调用HashMap的hash()函数,直接调用String的hashcode()不好吗?

HashMap中的hash()函数如下所示:1

2

3

4static final int hash(Object key){

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

HashMap中的hash()函数是将得到hashcode做进一步处理,它将hashcode的高16位和低16位进行异或操作,这样做的目的是:在table的长度比较小的情况下,也能保证hashcode的高位参与到地址映射的计算当中,同时不会有太大的开销。

综上所述:从hashcode计算得到table索引的计算过程如下所示:

put()方法的执行过程如下所示:

HashMap的扩容机制

在HashMap中有一下两个属性和扩容相关:1

2int threshold;

final float loadFactor;

其中threshold = Length * loadFactor,Length表示table数组的长度(默认值是16),loadFactor为负载因子(默认值是0.75),阀值threshold表示当table数组中存储的元素超过这个阀值的时候,就需要扩容了。以默认长度16,和默认负载因子0.75为例,threshold = 16 * 0.75 = 12,即当table数组中存储的元素个数超过12个的时候,table数组就该扩容了。

当然Java中的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,然后将旧数组中的元素经过重新计算放到新数组中,那么怎样对旧元素进行重新映射呢?

其实很简单,由于我们在扩容时,是使用2的幂扩展,即数组的长度扩大到原来的2倍, 4倍, 8倍…,因此在resize时(Length - 1)这部分相当于在高位新增一个或多个1bit,我们以扩大到原来的两倍为例说明:

(a)中n为16,(b)中n扩大到两倍为32,相当于(n - 1)这部分的高位多了一个1, 然后和原hash码作与操作,这样元素在数组中映射的位置要么不变,要不向后移动16个位置:

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

这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此resize的过程,均匀的把之前的冲突的节点分散到新的bucket了。这一块就是JDK1.8新增的优化点。有一点注意区别,JDK1.7中resize的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。

HashMap死锁形成原理

HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用线程安全的ConcurrentHashMap。

要理解HashMap死锁形成的原理,我们要对HashMap的resize里的transfer过程有所了解,transfer过程是将旧数组中的元素复制到新数组中,在Java 8之前,复制过程会导致链表倒置,这也是形成死锁的重要原因(Java 8中已经不会倒置),transfer的基本过程如下:1

2

3

41. 新建节点e指向当前节点,新建节点next指向e.next

2. 将e.next指向新数组中指定位置newTable[i]

3. newTable[i] = e

4. e = next

举个例子:1

2

3

4

5

6

7现在有链表1->2->3,要将它复制到新数组的newTable[i]位置

1. Node e = 1, next = e.next;

2. e.next = newTable[i];

3. newTable[i] = e;

4. e = next, next = e.next;

执行完后会得到这样的结果:

newTable[i]=3->2->1

死锁会在这种情况产生:两个线程同时往HashMap里放Entry,同时HashMap正好需要扩容,如果一个线程已经完成了transfer过程,而另一个线程不知道,并且又要进行transfer的时候,死锁就会形成。1

2

3

4

5现在Thread1已将完成了transfer,newTable[i]=3->2->1

在Thread2中:

Node e = 1, next = e.next;

e.next = newTable[i] : 1 -> newTable[i]=3

newTable[i] = e : newTable[i] = 1->3->2->1 //这时候链表换已经形成了

在形成链表换以后再对HashMap进行Get操作时,就会形成死循环。

在Java 8中对这里进行了优化,链表复制到新数组时并不会倒置,不会因为多个线程put导致死循环,但是还有很多弊端,比如数据丢失等,因此多线程情况下还是建议使用ConcurrentHashMap。

HashMap和Hashtable有什么区别

Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图所示:

Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

总结扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。

负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。

HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。

JDK1.8引入红黑树大程度优化了HashMap的性能。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LVM逻辑卷创建管理

在虚拟机中再次添加三张硬盘 1、查看添加的硬盘 [rootrhel-02 ~]# fdisk -l 2、添加分区 [rootrhel-02 ~]# fdisk /dev/sdb 查看分区并保存 3、将物理硬盘分区初始化为物理卷,以便LVM使用 如果没安装LVM的话先去安装 [rootrhel-02 ~]# yum install lvm2 安装完成…

Start DWM manually on Windows 7 and vista

方法一: 1. 检查两处注册表项及键值是否与下列数值一致 HKEY-Current-User\Software\Microsoft\Windows\DWM\Composition 键值改为 1 HKEY-Current-User\Software\Microsoft\Windows\DWM\CompositionPolicy 键值改为2 2. 打开运行(可能要用到管理员模式启…

java启动mysq服务_Java Web开发——MySQL数据库的安装与配置

MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系…

小程序如何获得手机号码_获得小型企业电话号码的最佳方法

小程序如何获得手机号码Lots of small businesses use their personal cellphones when making work related phone calls. Some may even be using old landlines for their calling needs. While it makes sense to use your cellphone, and it can be scary to make a chang…

空间数据索引RTree完全解析及Java实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/MongChia1993/article/details/69941783第一部分 空间数据的背景介绍 空间数据的建模 基于实体的模型(基于对象)Entity-based models (or object base…

Android 中的ORM框架

在android 中,内置了sqlite数据库,java web 中,用惯了Hibernate ,想找找android中是否也有类似的orm框架,后来在开源中国看到了orman,这是一个很不错的框架。 这个可以帮我们快捷方便的实现数据库的CURD操作…

android页面布局 如何让中间的listview填充剩余部分_谷歌驾驶设计—界面设计布局...

本节提供了可在不同屏幕尺寸范围内缩放的屏幕布局的设计指南。此处定义的padding和keyline值用于Components,Media规范、Notification Center规范和Dialer规范中。指南概览(TL:DR):基于适当的屏幕尺寸类别的基本布局使…

ios 禁用滑动手势_如何禁用笔记本电脑上的Windows 8滑动手势?

ios 禁用滑动手势If you’re not a fan of the touchpad-based swipe gestures in Windows 8 there is a way to completely disable them and reclaim your touchpad. 如果您不喜欢Windows 8中基于触摸板的滑动手势,可以使用一种方法来完全禁用它们并收回您的触摸板…

Java快速入门-01-基础篇

Java快速入门-01-基础篇 如果基础不好或者想学的很细,请参看:菜鸟教程-JAVA本笔记适合快速学习,文章后面也会包含一些常见面试问题,记住快捷键操作,一些内容我就不转载了,直接附上链接,嘻嘻开发…

Excel导入MS SQL SERVER 操作

关于Excel导入到sql操作的相关问题总结: 一、大批量数据导入 方法1、从Excel大批量数据导入时我们可以使用sql里面有一个batch copy的功能 方法2、在sql中建一个table type结构,在前端将excel读到datatable中,把整个datatable作为存储过程参数…

苹果mac闪退_自从Mac有了WPS,从此和双系统说再见!

薛岗13,712本文共计2266个字,预计阅读时长需要6分钟。大部分使用Macbook的用户都有一个痛点,就是编辑好的office文件,在朋友或同事的windows电脑上展示效果与自己的会有差异。除此外,卡顿、闪退、数据丢失等也是Windows版office在…

初学者计算机_初学者极客:如何在计算机上重新安装Windows

初学者计算机Reinstalling Windows is one of the easiest ways to fix software problems on your computer, whether it’s running slow or infected by viruses. You should also reinstall Windows before you get rid of an old PC. 重新安装Windows是修复计算机上软件问…

win7 32位 安装opencv-python后,运行时提示 from .cv2 import *: DLL load failed: 找不到指定的模块 的解决办法...

安装opencv后,运行一个测试程序提示"from .cv2 import *: DLL load failed: 找不到指定的模块"。于是百度一下解决办法,结果试了N多方法后也没能解决这个问题。 最后不得不耐心的下载了dependency walker来查看opencv到底是缺少了哪个dll文件。…

goahead处理json_GoAhead Web Server远程代码执行漏洞分析(附PoC)

*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。本文是关于GoAhead web server远程代码执行漏洞(CVE-2017-17562)的分析,该漏洞源于在初始化CGI脚本环境时使…

项目中的模块剥离成项目_使用MCEBuddy 2从电视录制中剥离广告

项目中的模块剥离成项目One of the great things about time-shifting your television viewing is that you are able to watch the shows you love at a time that suits you. Just because you have an appointment on Wednesday evening there’s no need to miss out on y…

有上下界限制可行流

无源汇有上下界限制可行流(循环流) 即每条边的流量限制为[L,R],判断有没有满足整个网络的可行流。 看看以前学的网络流,实际上它的流量限制为[0,C],现在无非多了一个下限的限制。 网络流的一个重要性质:除了…

.gitignore文件将已经纳入版本管理的文件删除

git rm -r --cached . git add . git commit -m update .gitignore git push -u origin master 先将本地缓存删除,再提交,.gitignore文件只针对那些没有被staged的文件有效 参考博客:https://www.cnblogs.com/kevingrace/p/5690241.html 转载…

gmail收件箱标签设置_通过在Gmail中启用实验室功能来启动收件箱

gmail收件箱标签设置We recently looked at how you can make it easier to manage multiple inboxes in Gmail using the Multiple Inboxes Lab feature. This is a non-standard feature and it’s far from being the only one available to you. In fact there are numerou…

linux rmp命令安装包在哪里_rpm命令_Linux rpm 命令用法详解:RPM软件包的管理工具...

rpm命令是RPM软件包的管理工具。rpm原本是Red Hat Linux发行版专门用来管理Linux各项套件的程序,由于它遵循GPL规则且功能强大方便,因而广受欢迎。逐渐受到其他发行版的采用。RPM套件管理方式的出现,让Linux易于安装,升级&#xf…

【题解】洛谷P1066 [NOIP2006TG] 2^k进制数(复杂高精+组合推导)

洛谷P1066:https://www.luogu.org/problemnew/show/P1066 思路 挺难的一道题 也很复杂 满足题目要求的种数是两类组合数之和 r的最多位数m为 w/k(当w mod k0 时)w/k1(当 w mod k1 时)First: 位数为2~m的种数 即从2k-1中…