HashMap及ConcurrentHashMap基本原理概述
0、前言
本博文部分文字及图片参考自以下三篇文章,其余内容为本人经过思考及总结后所写,仅作为学习分享使用,如有侵权,请联系本人删除,谢谢。
1、什么是HashMap
2、高并发下的HashMap
3、什么是ConcurrentHashMap?
1、HashMap基本原理
众所周知,HashMap是一个用于存储Key-Value键值对的集合,每一个键值对也叫做Entry。这些个键值对(Entry)分散存储在一个数组当中,这个数组就是HashMap的主干。
HashMap数组每一个元素的初始值都是Null。
对于HashMap,我们最常使用的是两个方法:Get 和 Put。
Put方法的原理
调用Put方法的时候发生了什么呢?
比如调用 hashMap.put("apple", 0) ,插入一个Key为“apple"的元素。这时候我们需要利用一个哈希函数来确定Entry的插入位置(index):
index = Hash(“apple”)
假定最后计算出的index是2,那么结果如下:
但是,因为HashMap的长度是有限的,当插入的Entry越来越多时,再完美的Hash函数也难免会出现index冲突的情况。比如下面这样:
这时候该怎么办呢?我们可以利用链表来解决。
HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的头节点。每一个Entry对象通过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只需要插入到对应的链表即可:
- 需要注意的是,新来的Entry节点插入链表时,使用的是“头插法”。之所以把Entry6放在头节点,是因为HashMap的发明者认为,后插入的Entry被查找的可能性更大。
Get方法的原理
使用Get方法根据Key来查找Value的时候,发生了什么呢?
首先依然会把输入的Key做一次Hash映射,得到对应的index:
index = Hash(“apple”)
由于刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。假设我们要查找的Key是“apple”:
第一步,我们查看的是头节点Entry6,Entry6的Key是banana,显然不是我们要找的结果。
第二步,我们查看的是Next节点Entry1,Entry1的Key是apple,正是我们要找的结果。
在这里get方法会沿着链表一直往下寻找,直到找到了key为apple的节点。若找到链表最尾端的时候(e.next=null)还找不到的话,则返回null
- 所以当此处出现双向循环链表的时候,那么程序就会出现死循环,因为e.next永远不会等于null
2、HashMap默认初始长度是多少,为什么?
默认长度是16,并且每次自动扩展或手动初始化时,长度必须是2的幂次方。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
之前说过,从Key映射到HashMap数组的对应位置,会用到一个Hash函数,如何实现一个尽量均匀分布的Hash函数呢?我们通过利用Key的HashCode值来做某种运算。
通常情况下,Key的HashCode值会是一个比较大的值,但我们HashMap的初始长度只有16,所以我们必须采取某些方法来将这个HashCode值和Map的长度值做一个映射转换
常见的做法就是将Key的HashCode值和Map的长度值进行求模运算,但模运算效率低,为了实现高效的算法,HashMap采用了位运算的算法。
如何进行位运算呢?有如下的公式(Length是HashMap的长度):
index = key.hashCode() & (Length - 1)
下面我们以“book"的Key来演示整个过程:
1、计算book的hashcode,结果为十进制的3029737,二进制的101110001110101110 1001。
2、假定HashMap长度是默认的16,计算Length-1的结果为十进制的15,二进制的1111。
3、把以上两个结果做与运算,101110001110101110 1001 & 1111 = 1001,十进制是9,所以 index=9。
可以说,Hash算法最终得到的index结果,完全取决于Key的Hashcode值的最后几位。
HashMap长度必须是2的幂次方,这样才能保证Length-1的二进制形式全是1。因为假如Length-1的值为1000的话,那么其他数和1000进行与运算之后,结果就只有1000或0000这两种情况,这样就会造成大量的冲突,显然不符合Hash算法均匀分布的原则。
3、HashMap的扩展
HashMap的容量是有限的。当经过多次元素插入,使得HashMap达到一定饱和度时,Key映射位置发生冲突的几率会逐渐提高。这时候,HashMap需要扩展它的长度,也就是进行Resize。
影响发生Resize的因素有两个:
1、Capacity:HashMap的当前长度。
2、LoadFactor:HashMap负载因子,默认值为0.75f。
衡量HashMap是否进行Resize的条件如下,也就是说当HashMap中存储的数据量超过总量的0.75倍的时候,则认为该HashMap已经超过负载,需要进行Resize:
Resize的步骤
1、扩容
- 创建一个新的Entry空数组,长度是原数组的2倍。
2、ReHash
遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。
让我们回顾一下Hash公式:
index = key.hashCode() & (Length - 1)
当原数组长度为8时,Hash运算是和111B做与运算;新数组长度为16,Hash运算是和1111B做与运算。Hash结果显然不同。
Resize前的HashMap:
Resize后的HashMap:
ReHash的Java代码如下:
/*** Transfers all entries from current table to newTable.*/ 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指向链表中的最后一个节点e.next = newTable[i];// 然后让e成为链表中的第一个节点newTable[i] = e;e = next;}} }
HashMap的扩展在多线程下会造成死循环
如当前HashMap中存在两个元素a和b,其中他们存放的地址冲突,即hash(a) = hash(b) = 0,此时该哈希表的内存图如下:
假如现在有两个线程分别对该HashMap执行put操作,此时HashMap由于容量不够就需要进行扩容了,假设线程1先执行,在执行完Entry<K,V> next = e.next;这一句之后,cpu的时间片切换到了线程2上了,并且线程2顺利地执行完毕,此时我们先看线程2执行完后的内存图是怎样的。
此时又轮到线程1执行了,我们回顾下rehash的代码
/*** Transfers all entries from current table to newTable.*/ void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;for (Entry<K,V> e : table) {while(null != e) {// 线程1执行完后停在了这里,e=0x001,next=0x009Entry<K,V> next = e.next;// 线程1又执行了// 与之前不同的是,原本是0x009.next = null,现在变成了0x009.next = 0x001if (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在while代码块中执行了三次
- 1、e=0x001,next=0x009,然后newTable[2] = 0x001,0x001.next = null
- 2、e=0x009,next=0x001,然后newTable[2] = 0x009,0x009.next = 0x001
- 3、e=0x001,next=null,然后newTable[2] = 0x001,0x001.next = 0x009
最终内存图如下:
此时当调用Get查找一个不存在的Key,而这个Key的Hash结果恰好等于2的时候,由于位置2带有环形链表,所以程序将会进入死循环!
4、ConcurrentHashMap
由于多线程在操作HashMap时会出现环形链表进而导致死循环的问题,所以此时就必须寻找解决方法。
Hashtable或Collections.synchronizedMap均能保证线程的安全性,但两者都使用了带有阻塞的悲观锁,性能不高。
在并发环境下,ConcurrentHashMap能到兼顾线程的安全性以及运行的效率,替代了Hashtable。
ConcurrentHashMap通过使用Segment的方式来减少悲观锁的产生。
其原理有点类似于jvm堆内存分配对象时所使用的本地线程分配缓冲(TLAB),每个线程有一个自己专属的区域,各个线程在自己的区域中执行代码,互不干扰。
ConcurrentHashMap则可以看成一个二级哈希表,首先其维护的哈希表中存储的均为Segment对象,而各个Segment对象中同时也维护了一个哈希表,哈希表里面存放的才是真正我们要用的entry对象。
如果两个线程同时操作两个Segment中的两个哈希表,那么自然也就不会出现线程安全性问题了,两者可以同时执行。
这样子设计之后,当我们put进一个元素时,就需要进行两次hash值的获取,第一次先获取key所对应的Segment的位置,第二次再获取key在Segment中所对应entry对象的真正位置。
当然,AB线程也有可能同时操作到同一个Segment,为了保障线程的安全性问题,Segment的写入是需要上锁的,因此对同一Segment的并发写入会被阻塞(由于只对写操作上锁,所以并发读或一个线程读一个线程写的情况并不会被阻塞)。
由此可见,ConcurrentHashMap当中每个Segment各自持有一把锁。在保证线程安全的同时降低了锁的粒度(降低了线程阻塞的可能性),让并发操作效率更高。
总结ConcurrentHashMap的get步骤和put步骤如下:
get
1、为输入的Key做Hash运算,得到hash值。
2、通过hash值,定位到对应的Segment对象
3、再次通过hash值,定位到Segment当中数组的具体位置。
put
1、为输入的Key做Hash运算,得到hash值。
2、通过hash值,定位到对应的Segment对象
3、获取可重入锁
4、再次通过hash值,定位到Segment当中数组的具体位置。
5、插入或覆盖HashEntry对象。
6、释放锁。
ConcurrentHashMap如何保障size()方法数据的一致性?
ConcurrentHashMap中的size方法是通过将各个Segment内部的元素数量汇总起来从而得出ConcurrentHashMap元素的总数量的。
假如size方法在统计完Segment1之后,准备统计Segment2的数量时,另一个线程往Segment1插入了一个元素,同时比size方法更先运行完毕。那么在size方法运行完成之后,所得出的数量值就会比map的总数量就会少了一个。那么ConcurrentHashMap是如何保证size方法数据的一致性的呢?
ConcurrentHashMap是使用了类似于CAS乐观锁的思想来保证size方法统计时不会出现问题,其步骤如下:
1、遍历所有的Segment,把所有Segment的修改次数累加起来(我们在看集合源代码的时候经常会看到modCount++的这个操作,其实modCount变量就是用来统计当前集合修改次数用的)。
2、在第一步遍历的时候,同时把Segment中内部的元素数量累加起来,得到size值。
3、再一次统计所有Segment修改次数的总和。
4、判断所有Segment的总修改次数是否大于我们第一步所统计的修改次数。如果大于,说明统计过程中有修改,重新统计(跳回第一步),记录尝试次数+1;如果不是。说明没有修改,统计结束。
5、如果尝试次数超过阈值,则对每一个Segment加锁,再重新统计。
6、再次判断所有Segment的总修改次数是否大于上一次的总修改次数。由于已经加锁,次数一定和上次相等。
7、释放锁,统计结束。
HashMap及ConcurrentHashMap基本原理概述相关推荐
- Java之HashMap系列--ConcurrentHashMap的原理
原文网址:Java之HashMap系列--ConcurrentHashMap的原理_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Java中的ConcurrentHashMap的原理. JDK7与J ...
- Docker基本原理概述
Docker基本原理概述 Docker是一个用于开发,交付和运行应用程序的开放平台.Docker能够将应用程序与基础架构分开,从而可以快速交付软件.借助Docker,可以以与管理应用程序相同的方式来管 ...
- Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的
多选 Hashtable,HashMap,ConcurrentHashMap都是Map的实现类,它们在处理null值的存储上有细微的区别,下列哪些说法是正确的:答案在文末 A. Hashtable的K ...
- Java7/8 中的 HashMap 和 ConcurrentHashMap
Java7 HashMap 数组+链表 Java7 ConcurrentHashMap Segment数组+HashEntry数组链表+ReenTrantLock分段锁 Java8 HashMa ...
- Java 7:HashMap与ConcurrentHashMap
从我过去有关性能的文章和HashMap案例研究中可能已经看到,Java线程安全性问题可以很轻松地使Java EE应用程序和Java EE容器崩溃. 在对Java EE性能问题进行故障排除时,我观察到的 ...
- HashMap与ConcurrentHashMap的测试报告
日期:2008-9-10 测试平台: CPU:Intel Pentium(R) 4 CPU 3.06G 内存:4G 操作系统:window server 2003 一.HashMap与Concurre ...
- java+线程安全的hash,多线程下HashMap安全问题-ConcurrentHashMap解析
Java1.5 引入了 java.util.concurrent 包,其中 Collection 类的实现允许在运行过程中修改集合对象.实际上, Java 的集合框架是[迭代器设计模式]的一个很好的实 ...
- HashMap、ConcurrentHashMap原理分析
集合(Collection)是编程中常用的数据结构,而并发也是服务器端编程常用的技术之一,并发总是离不开集合这类高级数据结构的支持.比如两个线程需要同时访问一个中间临界区(Queue),比如常会用缓存 ...
- HashMap与ConcurrentHashMap万字源码分析
HashMap与ConcurrentHashMap`源码解析 JDK版本:1.7 & 1.8 开发中常见的数据结构有三种: 1.数组结构:存储区间连续.内存占用严重.空间复杂度大 优点:因 ...
最新文章
- 苹果手机4g网速慢怎么办_2020 年双十一建议学生党买 4G 苹果手机吗?
- PHP如何将表单提交给自己
- 比特位计数—leetcode338
- SAP Cloud for Customer的Calculated field字段
- 微信小程序 悬浮按钮
- 提高C++程序运行效率的10个简单方法
- windows下将多个文件里面的内容合并成一个一个文件
- JAVA处理模型的步骤,java-处理模型中条件字段的最佳方法
- Linux系统管理--Centos6服务管理
- 航空系统c语言课程设计报告,c语言课程设计报告_航空订票系统西安郵電學院.doc...
- 微弱信号检测_第二版-高晋占
- 售前是做什么的?需要具备什么能力?
- vx开发|JSON配置文件
- 【LearnOpenGL】-PBR材质
- java.lang.RuntimeException: Method w in android.util.Log not mocked Androidstudio单元测试配置build.gradle
- 【掌上齐齐哈尔】市民网上换领驾驶证方便快捷
- audition cc变声插件_Adobe Audition CC怎么安装插件?
- c++数独游戏2.0
- Linux环境下安装Oracle 11g R2完整图文教程
- 快速排序 php内存超限,数据结构与算法设计
热门文章
- 突然!华为P30 Pro真机上手视频曝光:屏幕指纹解锁秒开
- 苹果最新专利曝光:苹果可能正研发可折叠iPhone
- SQL COUNT() 语法总结及用法【原创】
- the android sdk location cannot be at the filesystem root
- Php 取出session中的值,获取php值
- vue 给checkbox 赋值_浅谈vue中关于checkbox数据绑定v-model指令的个人理解
- centos php 开启libgdgd_CentOS6.5安装Nginx1+MySQL5+PHP5
- vue-cli目录结构介绍
- 【MyBatis】MyBatis Order By 字段动态动态排序
- 【Elasticsearch】 Elasticsearch对外提供分词服务实践