Java —— HashMap

32 篇文章 1 订阅

        HashMap的底层主要是基于数组和链表来实现的。

一、put过程

  1. 对 Key 求 Hash 值,然后再计算下标
  2. 如果没有碰撞,直接放入数组中
  3. 如果碰撞了,以链表的方式链接到后面
  4. 如果链表长度超过阀值(8),就把链表转成红黑树,链表长度低于6,就把红黑树转回链表
  5. 如果节点已经存在就替换旧值
  6. 如果数组已满(容量16*加载因子0.75),就需要 resize(扩容2倍后重排)

二、get过程        

        当我们调用 get() 方法,HashMap 会使用键对象的 hashcode 找到数组位置,找到之后,会调用keys.equals() 方法去找到链表中正确的节点,最终找到要找的值对象。

三、如何减少碰撞

        原理是如果两个不相等的对象返回不同的 hashcode 的话,那么碰撞的几率就会小些。

        提高碰撞下的寻址性能,可以不用二叉查找树,而选择红黑树?为什么不一直使用红黑树?

        之所以选择红黑树是为了解决二叉查找树的缺陷:二叉查找树在特殊情况下会变成一条线性结构(这就跟原来使用链表结构一样了,造成层次很深的问题),遍历查找会非常慢。而红黑树在插入新数据后可能需要通过左旋、右旋、变色这些操作来保持平衡。引入红黑树就是为了查找数据快,解决链表查询深度的问题。我们知道红黑树属于平衡二叉树,为了保持“平衡”是需要付出代价的,但是该代价所损耗的资源要比遍历线性链表要少。所以当长度大于8的时候,会使用红黑树;如果链表长度很短的话,根本不需要引入红黑树,引入反而会慢。

(一)红黑树

  • 每个节点非红即黑
  • 根节点总是黑色的
  • 如果节点是红色的,则它的子节点必须是黑色的(反之不一定)
  • 每个叶子节点都是黑色的空节点(NIL节点)
  • 从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)

四、解决 hash 碰撞还有那些办法?

        开放定址法

        当冲突发生时,使用某种探查技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的地址。按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、二次探查法、双重散列法等。

五、如果 HashMap 的大小超过了负载因子定义的容量怎么办?

        HashMap 默认的负载因子大小为0.75。也就是说,当一个 Map 填满了75%的数组的时候,将会创建原来 HashMap 大小的两倍的 bucket 数组来重新调整 Map 大小,并将原来的对象放入新数组中。这个过程叫作 rehashing。

        因为它调用 hash 方法找到新的 bucket 位置。这个值只可能在两个地方,一个是原下标的位置,另一种是在下标为 <原下标+原容量> 的位置。

六、HashMap的Resize具体做了哪些事情呢?

  • 扩容:创建一个新的Entry空数组,长度是原数组的2倍。

  • ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。为什么要重新Hash呢?因为长度扩大以后,Hash的规则也随之改变。 

七、HashMap 与 HashTable 区别

        线程安全性:HashTable 安全

        效率不同:HashTable 要慢,因为加锁

        Hashtable 使用了 synchronized,保证线程安全

八、CocurrentHashMap 与Hashtable 

        我们知道 Hashtable 是 synchronized 的,但是 ConcurrentHashMap 同步性能更好,因为它仅仅根据同步级别对 map 的一部分进行上锁

        ConcurrentHashMap 当然可以代替 HashTable,但是 HashTable 提供更强的线程安全性

        它们都可以用于多线程的环境,但是当 Hashtable 的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。由于 ConcurrentHashMap 引入了分割(segmentation),不论它变得多么大,仅仅需要锁定 Map 的某个部分,其它的线程不需要等到迭代完成才能访问 Map。简而言之,在迭代的过程中,ConcurrentHashMap 仅仅锁定 Map 的某个部分,而 Hashtable 则会锁定整个 Map

        CocurrentHashMap(JDK 1.8)

        CocurrentHashMap 抛弃了原有的 Segment 分段锁,采用了 CAS + synchronized 来保证并发安全性。其中的 val next 都用了 volatile 修饰,保证了可见性。

        最大特点是引入了 CAS

         CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。

         CAS的ABA问题

        并发1(上):获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功

        并发2:将数据修改成B

        并发3:将数据修改回A

        并发1(下):CAS乐观锁,检测发现初始值还是A,进行数据修改

        ABA问题可以使用带参数版本解决,在读取和替换时判断版本

        上述并发环境下,并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。

        CAS 使用实例

        对 sizeCtl 的控制都是用 CAS 

        在开始初始化的时候,首先判断sizeCtl的值,如果sizeCtl < 0,说明有线程在初始化,当前线程便放弃初始化操作。否则,将SIZECTL设置为-1,Hash表进行初始化。初始化成功以后,将sizeCtl的值设置为当前的容量值。

九、hashcode的生成过程

        hashcode是通过将内部存储地址映射成一个整型值,这个整型值就是hashcode。

        

十、hashmap为什么是2的次幂?

        hashMap源码获取元素的位置:计算Entry对象保存在 table中的数组索引值:

        返回的是某个hashcode对应的下标位置

static int indexFor(int h, int length) {

// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

return h & (length-1);

}

        解释:

        h:为插入元素的hashcode

        length:为map的容量大小

        &:与操作 比如 1101 & 1011=1001

        如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快。

十一、在多线程环境下使用hashMap会产生什么问题?

        会产生死循环,造成死循环的核心是因为数组转移的时候采用了头插法,在多线程环境下,在扩容的时候造成某个key会被转移两次,从而形成循环链表。JDK1.8中将头插法改成了尾插法,避免了这个问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宇宙超级无敌程序媛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值