HashMap底层分析

HashMap底层分析

我们先来看一下他的组成

hashmap底层是由数组跟链表组成的(这是在jdk1.7之前)

在jdk17.之后还引用了红黑树

JDK1.8中:

**使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构**

如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里。

如果同一个格子里的key不超过8个,使用链表结构存储。

如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。

那么即使hashcode完全相同,由于红黑树的特定,查找某个特定元素,也只需要O(log n)的开销

也就是说put/get的操作的时间复杂度只有O(log n)

**备注:当数组大小已经超过64并且链表中的元素个数超过默认设定(8个)时,将链表转化为红黑树**

,数组主要是用来存hashcode的值,链表主要是用来解决hash冲突,对于hash冲突的解决主要有四种方法1.开放寻\定址法2.再哈希发3.链地址法4.建立公共溢出区

hash冲突是什么呢?

hash冲突主要是因为散列表(也可以理解为存储hash值的表)是有限的,当数据足够大的时候,难免会出现hashcode相同的时候,这个就叫hash冲突.

简而言之就是两个不同数据,通过一定的计算,得到的hashcode是相同的,不过一般不会相同,我们要避免相同的情况,因而就有了解决hash冲突的方法

解决hash冲突的方法

上面我们已经提到过了有四种方法解决hash冲突

  1. 开放定址法:

    所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
    公式为:fi(key) = (f(key)+di) MOD m (di=1,2,3,……,m-1)
    ※ 用开放定址法解决冲突的做法是:当冲突发生时,使用某种探测技术在散列表中形成一个探测序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探测到开放的地址则表明表 中无待查的关键字,即查找失败。
    比如说,我们的关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12。

    我们用散列函数f(key) = key mod (表长) 即12

    从上述集合种我们每一个元素都按照这个函数进行计算,

    第一个元素12 计算完下标为0,对应如下

    下标012345678910
    关键字12

    第二个元素67 计算完下标为7,对应如下

    下标012345678910
    关键字1267

​ 当计算前S个数{12,67,56,16,25}时,都是没有冲突的散列地址,直接存入:

下标012345678910
关键字1225166756

接着往下走,当计算到37这个值时,发现f(37) = 1,此时就与25所在的位置冲突。 于是我们应用上面的公式f(37) = (f(37)+1) mod 12 = 2。于是将37存入下标为2的位置.这就是 开放定址法

下标012345678910
关键字122537166756
  1. 再哈希法

    再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数,计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。

    即通过不同的算法去得到不同的下标.

  2. 链地址法

    链地址法的基本思想是:每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,如:
    键值对k2, v2与键值对k1, v1通过计算后的索引值都为2,这时及产生冲突,但是可以通道next指针将k2, k1所在的节点连接起来,这样就解决了哈希的冲突问题

在这里插入图片描述

  1. 建立公共溢出区:

这种方法的基本思想是:将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

HashMap的扩容原理

当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,通过一个resize()方法就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

总得来说,就是拷贝旧的数据元素,从新新建一个更大容量的空间,然后进行数据复制!

Hashmap为什么多线程不安全

因为hashmap在扩容时会产生环形链表,什么是环形链表,为什么会产生环形链表呢?

主要在这扩容的过程。当多个线程同时对这个HashMap进行put操作,而察觉到内存容量不够,需要进行扩容时,多个线程会同时执行resize操作,而这就出现问题了,问题的原因分析如下:

首先,在HashMap扩容时,会改变链表中的元素的顺序,将元素从链表头部插入。PS:说是为了避免尾部遍历,这一部分不是本博客的主要介绍内容,后面再说。

而环形链表就在这一时刻发生,以下模拟2个线程同时扩容。假设,当前hashmap的空间为2(临界值为1),hashcode分别为0和1,在散列地址0处有元素A和B,这时候要添加元素C,C经过hash运算,得到散列地址为1,这时候由于超过了临界值,空间不够,需要调用resize方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过程如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hBBprQN3-1626310911447)(image-20210610102918163.png)]

在这里插入图片描述

在这里插入图片描述

这个过程为,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,B.next=A,所以,这里继续复制A,让A.next=B,由此,环形链表出现:B.next=A; A.next=B

既然hashmap不安全,于是有了ConcurrentHashMap

下面我们来谈一下为什么要使用ConcurrentHashMap。

在并发编程中使用HashMap可能导致程序死循环。而使用线程安全的HashTable效率又非常低下,基于以上两个原因,便有了ConcurrentHashMap的登场机会

1)线程不安全的HashMap

在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。

2)效率低下的HashTable

HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态。如线程1使用put进行元素添加,线程2不但不能使用put方法添加元素,也不能使用get方法来获取元素,所以竞争越激烈效率越低。

3) ConcurrentHashMap的锁分段技术可有效提升并发访问率

HashTable容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable的线程都必须竞争同一把锁,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

深入了解ConcurrentHashMap

并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术。首先将数据分成一段一段地存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

深入了解ConcurrentHashMap

见ConcurrentHashMap底层

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值