HashMap原理详解:探测技术(Probing)、数据聚集(Clustering)、寻址方式(Addressing)、墓碑删除(tombstones)等技术的深度剖析
一、前言
看到标题大家都应该觉得奇怪,我们去面试被问到HashMap的实现,大家不都是说的基于数组+链表的方式么。为什么我们会说HashMap不是基于数组+链表的方式实现的呢?其实这是大家的狭义理解导致的。真正的HashMap是广义的概念,我们平常所说的HashMap都是只Java里面的HashMap实现。这只是所有HashMap实现方法中的一种。
广义的HashMap从寻址方式上分为Open Addressing HashMap和Closed Addressing HashMap。而Open Addressing又根据探测技术细分为:Linear Probing、Quadratic Probing和Double Hashing等。在Open Addressing中又有Primary Clustering和Secondary Clustering的概念。
看到这些概念想必很多同学都已经晕了,是不是发现很多概念都没有听过。接下来楼主就为大家详细讲解广义HashMap的概念和底层实现原理。
二、广义HashMap的原理
1、寻址方式
首先我们简单介绍下什么是寻址方式。对Java HashMap有了解的同学对寻址方式应该不陌生。在Java HashMap中,其主要结构是数组+链表。我们根据Hash查找一个key应该落在数组中的哪个位置的过程就叫做寻址。寻址即如何找到Key在数组中的位置。因为所有的HashMap能够快速找到数据基础都是数组的快速定位。
广义的HashMap基于寻址方式的不同,将HashMap分为两大类:Open Addressing(开放寻址)和Closed Addressing(闭合寻址)。下面我们将对其进行深入讲解。
A、Closed Addressing(闭合寻址)
这种寻址方式大家容易理解。因为Java HashMap中的寻址方式就是Closed Addressing。其特点为:无论什么元素(元素的key)只要其通过hash定位到数组的某个位置,那么它必须放在这个位置。当出现Hash冲突的时候,我们可以使用额外存储空间来解决,如Java HashMap中使用链表的方式。
B、Open Addressing(开放寻址)
开发寻址方式是相对闭合寻址方式而言。即当我们通过hash的方式将元素(元素的key)定位到数组的某个位置时,我们不必一定将该元素放在数组的这个位置。而是可以通过其他的寻址方式(后续会讲解)将其放到数组的其他位置。
通过下图我们就可以明显看出两种寻址方式的不同(其中数字0-9表示数组的位置,而圆形圈中的数组则表示对应的元素)。
C、Open Addressing vs Closed Addressing
通过上面的分析我们可以看到Open Addressing和Closed Addressing有比较明显的区别。下面做了基本的总结:
对比项 | Open Addressing | Closed Addressing | 说明 |
元素容量 | 最大为数组大小 | 最大会比数组大小大很多 | 不考虑扩容的场景 |
数据聚集 | 存在数据聚集问题 | 不存在数据聚集问题 | 说明是数据聚集参考后面的章节 |
缓存行利用 | 可以充分利用缓存行 | 不能利用缓存行 | 利用缓存行可以提高读取性能 |
空间占用 | 仅为数组大小 | 会占用比数组大小更多的空间 | 不考虑元素自身大小 |
高load factor性能 | load factor=0.9时性能都极好 | load factor=0.75就需要扩容 | load factor:数组元素相对数组大小的占比 |
说明:
- 因为Closed Addressing会通过链表等额外空间的方式存储元素,所以它的容量比较大。可以说上限
- 数据聚集(后续会介绍)会导致读取数据慢
- Open Addressing的数据都放在数组中,数组结构可以充分利用CPU能力被加载到同一个缓存行中,从而提高读取性能
- Closed Adressing有链表等额外结构空间,会导致占用更多的内存。
- Closed Addressing使用链表解决Hash冲突,导致高load factor下链表很长,且不能利用缓存行导致查询性能很差。而Open Addressing的数据都在一个数组中,且能够利用缓存行,同时好的Open Addressing实现能够很好的平衡数据聚集问题,所以其在load factor=0.9时都能够有很好的性能。
通过上面的对比我们可以看到Open Addressing还是有很多优势的。特别是在高load factor下的性能表现,再加上大家都Closed Addressing的代表实现Java HashMap应该是非常的熟悉了,且网上也有很多文章讲解Java HashMap,因此本文就不再对Closed Addressing做进一步讲解。接下来我们重点讲解大家比较陌生的Open Addressing HashMap。
2、Open Addressing的探测技术
因为Open Addressing没有额外空间来解决hash冲突的问题,因此当存在Hash冲突的时候,其需要在数组中为这个元素找到一个空位置。这个当遇到hash冲突去寻找空位置的过程就叫做探测。这里使用的技术就叫做寻址技术。目前比较常用的寻址技术有Linear Probing、Quadratic Probing和Double Hashing。接下来我们详解对齐进行介绍。
A、Linear Probing(线性探测)
线性探测的方式比较简单。当写入元素的时候出现hash冲突时,我们直接去查看该位置的下一个是否可用,如果可用则直接插入元素。否则继续查找该位置的下一个,一直这样循环处理,知道找到合适的位置,或者触发Rehash。
这种探测技术计算可用位置的公式如下(i为通过hash确定的初始位置):
- i + 1
- i + 2
- i + 3
- …
这种探测方式存在一个问题。它会导致大量的元素聚集在一块,形成一个连续的链(物理地址上连续,不是链表)。当我们查找的数据在这个链里面的时候,需要不断一个一个查找。如果链越长,则查询的效率则越低。这种数据聚集在一起的现象就叫做聚集(Clustering),也可以叫做Primary Clustering。
B、Quadratic Probing(二次方探测)
二次方探测也比较简单,就是每次计算可用位置的时候不是直接+1,而是加二次方。
这种探测技术计算可用位置的公式如下(i为通过hash确定的初始位置):
- i + 1*1
- i + 2*2
- i + 3*3
- …
这种方式通过这种次方跳跃的方式寻找可用位置,虽然不容易产生Primary Clustering。但是也会产生另外一种链,比如hash冲突很严重(大量元素的hash到同一个位置),那么这些元素也会构成一个链(物理上不连续),在查找的时候仍然会导致查询慢的问题。这种数据链也是一种数据聚集。而这种数据聚集就叫做Secondary Clustering。
C、Double Hashing(二次Hash探测)
二次Hash探测顾名思义,就是当出现hash冲突的时候通过另外一个hash来计算下一个可用位置。
这种探测技术计算可用位置的公式如下(i为通过hash确定的初始位置,j=另外一个hash(key)值):
- i + 1 × j
- i + 2 × j
- i + 3 × j
- …
这种探测技术,相对来说就不会出现Primary Clustering和Secondary Clustering了。具体原因大家可以思考一下。
D、Linear Probing vs Quadratic Probing vs Double Hashing
探测技术 |
冲突slot计算方式 |
优点 |
缺点 |
Linear Probing |
|
很好的利用缓存行 |
存在primary clustering问题 存在secondary clustering问题 |
Quadratic Probing |
|
有间隙可以避免primary clustering |
存在secondary clustering问题 无法利用缓存行 |
Double Hashing |
j=hash2(key)
|
避免了primary clustering问题 避免了secondary clustering问题 |
无法利用缓存行 性能差(多一次hash) |
3、Clustering元素聚集
元素聚集就是只元素在数组上形成一个链,可以是物理上连续的,也可以是物理上不连续的。这种链会导致数查询的时候性能降低。就像Java HashMap中一样,当链表变长了之后HashMap的查询效率就会降低。
A、Primary Clustering
就是说元素聚集发生在物理上的连续,即在数据上的元素相邻挨着一起,中间无间隔。
B、Secondary Clustering
表示数组上的元素根据探测技术的算法形成了一个物理上不连续,但是在探测算法上连续的链。即通过通过探测技术的探测可用空位时,发现多次计算的位置都是被占用的,这就形成了一个物理上不连续但是逻辑上连续的链。
三、Open Addressing的增删查
Open Addressing的增删查和咱们熟知的Closed Addressing还是有很多不同。接下来我们就以线性探测(Linear Probing)为基础对Open Addressing Hash Map的增删查分别多进一步的讲解。
1、删除元素
在Open Addressing中要删除元素主要涉及到三种场景,如下图:
场景1:元素独立存在(图中元素12)
由于该元素独立存在于数组中,我们只需要直接将其从数组中删除接口。
场景2:元素在Clustering末尾(图中元素9)
这种场景和场景1是一样的,我们可以直接将其从数组中删除即可。
场景3:元素在Clustering中(图中元素6)
这种场景和前面的场景则不一样了。如果我们直接将元素6从数组中删除。则会导致一个问题,即下次查询的时候会查询不到7和8。因为由于6被删除,查询的时候查询到空位置则终止了(参考后续查询逻辑)。
这个问题的解决办法有如下两种:
tombstones法:即被删除的元素不直接从数组中删除,而是标记为不可用,查询的时候仍然可以继续向后查询到元素7和8。这样被标记删除的元素叫做tombstones,即墓碑。
backward shift deletion法:元素6被删除之后,该链中后面的元素都向前移动一格。这样元素4、5、7、8就挨着一起了。也不会影响后续的查询。
这两种删除方式各有各优缺点。
删除方式 | 执行速度 | 查询性能影响 | 性能测试 |
tombstones | 直接标记,速度快 | 删除元素不删除,会导致链很长,影响查询性能 | 罗宾汉墓碑法 |
backward shift deletion | 需要移动元素,速度慢 | 删除元素被删除,不会影响查询性能 | 罗宾汉backward shift法 |
PS:墓碑法的性能比较低,直接使用backward shift deletion法,其性能很高。截图可以参考中连接关于罗宾汉HashMap的两种不同删除元素的性能测试连接。
2、查找元素
查找元素就比较简单了,首先根据hash直接定位到数组的位置,然后对key进行equal比较。如果key不匹配则通过对应的探测技术继续探测下一个位置。如果在探测的过程中发现了墓碑(tombstones)元素,则跳过,即寻找下一个。一直循环要么找到对应的元素,要么找到一个空位置结束。
3、插入元素
首先根据hash定位到数组的位置,如果该位置为空或者为墓碑(tombstones),则将元素放在该位置,结束插入操作。如果不为空或者墓碑,则通过对应的探测技术继续寻找下一个位置,如此循环直到找到空位置或者墓碑位置,则插入元素。或则触发rehash,重新查找。
关于广义HashMap的技术与原理就介绍到这里,下期我们将介绍Open Addressing HashMap的具体实现例子:ThreadLocal和Robin Hood HashMap。
四、惯例
如果你对本文有任何疑问或者高见,欢迎添加公众号lifeofcoder共同交流探讨(添加公众号可以获得楼主最新博文推送以及”Java高级架构“上10G视频和图文资料哦)。
HashMap原理详解:探测技术(Probing)、数据聚集(Clustering)、寻址方式(Addressing)、墓碑删除(tombstones)等技术的深度剖析相关推荐
- 【Java基础】HashMap原理详解
[Java基础]HashMap原理详解 HashMap的实现 1. 数组 2.线性链表 3.红黑树 3.1概述 3.2性质 4.HashMap扩容死锁 5. BATJ一线大厂技术栈 HashMap的实 ...
- HashMap原理详解(基于jdk1.8)
HashMap原理详解(基于jdk1.8) HashMap原理详解,有兴趣的同学可以看下.有错误的地方也希望大佬们能指点下. HashMap的内部存储是一个数组(bucket),数组的元素Node实现 ...
- HashMap 原理详解
一.HashMap的原理详解 首先我们要知道什么是哈希表以及它的结构.在介绍哈希表之前我们需要了解并且掌握数组.链表以及红黑树的结构以及特点. 1.我们先来看一下HashMap的使用 public c ...
- Java集合篇:HashMap原理详解(JDK1.7及之前的版本)
(本文有关HashMap的源码都是基于JDK1.6的) 摘要: HashMap是Map族中最为常用的一种,也是 Java Collection Framework 的重要成员.本文首先给出了 Hash ...
- Java集合篇:HashMap原理详解(JDK1.8)
概述 JDK 1.8对HashMap进行了比较大的优化,底层实现由之前的"数组+链表"改为"数组+链表+红黑树",本文就HashMap的几个常用的重要方法和JD ...
- HashMap原理详解
一.hashmap简介 hashmap是Java当中一种数据结构,是一个用于存储Key-Value键值对的集合,每一个键值对也叫作Entry. 二.JDK7的HashMap 1.JDK7时HashMa ...
- java源码系列:HashMap底层存储原理详解——4、技术本质-原理过程-算法-取模具体解决什么问题
目录 简介 取模具体解决什么问题? 通过数组特性,推导ascii码计算出来的下标值,创建数组非常占用空间 取模,可保证下标,在HashMap默认创建下标之内 简介 上一篇文章,我们讲到 哈希算法.哈希 ...
- 大数据是什么和大数据技术十大核心原理详解
一.数据核心原理 从"流程"核心转变为"数据"核心 大数据时代,计算模式也发生了转变,从"流程"核心转变为"数据&quo ...
- 大数据技术十大核心原理详解
一.数据核心原理--从"流程"核心转变为"数据"核心 大数据时代,计算模式也发生了转变,从"流程"核心转变为"数据"核心 ...
- Java HashMap的实现原理详解
HashMap是Java Map类型的集合类中最常使用的,本文基于Java1.8,对于HashMap的实现原理做一下详细讲解. (Java1.8源码:http://docs.oracle.com/ja ...
最新文章
- 模型树——就是回归树的分段常数预测修改为线性回归 对于非线性回归有较好的预测效果...
- URI URL 简介区别
- oracle 回收undo,oracle 释放undo空间
- Codeforces Round #542 [Alex Lopashev Thanks-Round] (Div. 1)
- excel数据透视表列名更改
- 敏捷开发框架_他山之石-敏捷开发管理框架在设计项目中的应用
- mysql一些操作个人备忘(持续更新)
- 有着 30 多年经验的程序员最终被辞退了
- Spark 解析 : DAGScheduler中的DAG划分与提交
- 使用Automake,Autoconf生成Makefile
- (持续更新)C语言笔试题整理
- 笔记本电脑修改BIOS及刷写教程
- DRILLNET 2.0------第十三章 尾管固井扭矩/摩阻模型
- 算法训练Day24 | 回溯算法理论基础;LeetCode77.组合(经典的回溯问题)
- 计算机各键的名称和作用,space是什么键 键盘键位名称及功用详解
- lower_bound()/upper_bound()函数(C++)
- 期货开户需要具备⼀定的条件
- H.264 高度压缩数字视频编解码器标准
- 应用技术大公开系列Q之十一:(纤维).石墨烯纸制备工艺 (*3-4)
- openwrt旁路由的设置