HashMap和ConcurrentHashMap底层实现原理以及JDK1.7和1.8之间的区别

HashMap原理:

HashMap的底层结构:
在JDk1.7是数组+链表,JDK1.8之后则是数组+链表+红黑树结构
它不是线程安全的,核心的点就是put插入数据的过程,get查询数据以及扩容的方式。
Q:为什么要有链表: 因为不同的key可能存在相同的hash值,就会形成链式存储。
在这里插入图片描述
put元素: 在put插入的时候会根据key的hashcode去做hash运算得到一个index值(利用元素key的哈希值对数组长度-1按位与操作得到,为什么不用取模%运算呢?因为java的%比位运算慢10倍左右),根据index值放入数组的相应位置。每一个节点(Node)都会保存自身的hash、key、value、以及下个节点

put的头插和尾插: JDK1.7和1.8的主要区别在于头插和尾插方式的修改,多线程下头插容易导致HashMap链表死循环,resize后前后链表逆序 ,在转移过程中修改了原来链表中节点的引用关系。但是如果使用尾插,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了,并且1.8之后加入红黑树对性能有提升。

get元素的过程

  1. 首先向get()方法中传递一个key

  2. 对key的hashCode()做hash运算,计算在数组中的index;

  3. 在get()方法中调用getNode(hash,key)方法,找到对应的hash桶的位置,判断第一个节点是不是直接命中,如果是直接返回,如果有冲突,则往下遍历通过key.equals(k)去查找对应的Entry。
    若为树,则在树中通过key.equals(k)查找,O(logn);
    若为链表,则在链表中通过key.equals(k)查找,O(n)。

HashMap的扩容机制(resize)

有两个因素:

  • Capacity:HashMap长度。默认初始化长度是16。
  • LoadFactor:负载因子,默认值0.75f。0.75是为了平衡空间和时间的利用率,为了最大程度避免哈希冲突:负载因子太小(0.5),虽然时间效率提升了hash冲突减少,但是空间利用率降低了,负载因子是1.0的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利。

分为两步:

  • 1.扩容:创建一个新的Entry空数组,长度是原数组的2倍。并将容器指针(table)指向新数组。
  • 2.ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。(重新hash的原因是因为,长度扩大以后,hash的规则也随之改变了)

Q:hashmap冲突怎么解决?

1、开放定址法:是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能够找到,并将其记录存入。

2、链地址法(又称链接法/拉链法):将所有的哈希地址冲突的记录存储在同一个线性链表中。(hashmap)

3、再哈希法

HashMap的初始容量是2的n次幂,扩容也是2倍的形式进行扩容,是因为容量是2的n次幂,可以使得添加的元素均匀分布在HashMap中的数组上,减少hash碰撞

Q:hashmap什么时候链表转红黑树,什么时候红黑树转回链表?

在链表元素数量超过8时改为红黑树,为6的时候退转为链表。中间有个差值7可以防止链表和树之间频繁的转换

HashMap源码作者通过泊松分布算出,当桶中结点个数为8时,出现的几率是亿分之6的,因此常见的情况是桶中个数小于8的情况,此时链表的查询性能和红黑树相差不多,因为转化为树还需要时间和空间,所以此时没有转化成树的必要。

ConcurrentHashMap:

在JDK1.7中,ConcurrentHahMap底层是一个分段数组,采用分段锁Segment+HashEntry的方式进行实现。Segment是继承于ReentrantLock,来保证线程安全的。锁的粒度比较大,锁住整个Segment的,里面包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,Segment的初始化容量是16。

put和 get 经过两次Hash运算到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后再遍历entry链表。

put 操作JDK1.7:
先定位Segment,再定位桶,put全程加锁,没有获取锁的线程提前找桶的位置,并最多自旋64次获取锁,超过则挂起。

在这里插入图片描述

在JDK1.8中,它改成了与HashMap一样的数据结构,Node数组+单链表或者红黑树的数据结构(多了同步的操作),放弃了Segment分段锁机制,而是采用CAS+Synchronized**来保证并发安全进行实现,**Synchronized只锁定当前链表或红黑二叉树的头节点,这样只要hash不冲突,就不会产生并发。

put操作JDK1.8:
由于移除了Segment,类似HashMap,可以直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)

并发处理中先使用的是CAS,当有冲突的时候升级成Synchronized。

在这里插入图片描述

get()操作基本类似,由于value声明为volatile,保证了修改的可见性,因此不需要加锁。

如果我的文章对您有帮助的话,请点个赞吧!

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值