【HashMap】基本概念及扩容机制

基本概念

由数组和链表组成。

数组中每一个元素在java7叫entry,在java8叫node。

每一个元素都是k-v结构。

每一个节点都会保存自身的hash、key、value以及下个节点。

hashmap的特性。

1、允许空键和空值(空键只能有一个,空值可以有多个)。

2、元素是无序的,且所在位置是会变化的。

resize触发条件

数组容量有限,到达一定数量就会进行扩容(resize)。

根据HashMap当前长度(capacity)与负载因子(loadfactor)判断是否需要扩容。

负载因子默认为0.75f。

只要插入元素达到当前长度的0.75,就会进行扩容。

resize执行过程

1、创建一个新的Entry空数组,长度是原数组的两倍。

2、ReHash,遍历原来的Entry数组,把所有的Entry重新Hash到新数组。

rehash的原因,是因为长度扩大以后,hash规则会改变。

Hash的公式—> index = HashCode(Key) & (Length - 1)

原先为N,扩容后为2N,因此就存在低位部分0-(n-1),高位部分n-(2n-1),所以在扩容时分为low head和high head。

resize后,链表长度变小。

插入元素方法

元素要插入进来,首先计算key的Hash值,确定该元素的index,也就是应该插在哪个位置。

如何将元素插入链表中?

有两种方法,分别是头插法和尾插法。

头部插入法

在java8之前是头插法,新来的值会取代原有的值,原有的值往下走。
因为新插入的值被查找的可能性更大一点。

头插法的弊端

多线程环境下,有可能出现环形链表,A元素指向B,B元素指向A。
比如有三个线程,插入ABC三个元素。

在扩容前A->B->C,扩容后,hashmap是线程不安全的,三个线程操作map结构是没有顺序可言,有可能A指向B了,与此同时,B也指向A了。

具体原因参考:

https://blog.csdn.net/numbbe/article/details/113697731

尾部插入法

在java8之后尾部插入,插入到链表底部进行插入。这种方法不会改变链表中各节点的顺序。

两种方法对比

插队与排队的区别。

头插就是插队买票,容易被打。

尾插就是排队买票,没有插入的操作,乖乖后边排队就好。

两种方法的不同就在于resize时的rehash。

扩容时,在旧数组到新数组迁移的过程中,如果rehash之后的index还是一致。

那么由于头插法导致新数组链表顺序与原来相反,线程在坐链表循环时,又循环到了原来的值,形成环状。

使用头插会改变链表上的顺序,使用尾插,在扩容时会保持链表元素原本的顺序,不会出现链表成环的问题。

多线程操作HashMap时可能发生死循环,扩容转移后链表顺序倒置,改变了链表中节点的引用关系。

解决hashmap线程不安全的问题

HashTable、Collections.synchronizedMap、以及 ConcurrentHashMap 可以实现线程安全的 Map。

HashTable 在操作方法上加 synchronized 关键字,锁住整个数组,粒度较大。

Collections.synchronizedMap 使用 Collections 集合工具的内部类,通过传入 Map 封装出一个 SynchronizedMap 对象,内部定义了一个对象锁,方法内通过对象锁实现。

ConcurrentHashMap 使用分段锁,降低了锁粒度,提高并发度。

ConcurrentHashMap 的分段锁的实现原理

ConcurrentHashMap 成员变量使用 volatile 修饰,保证可见性和有序性。

另外使用 CAS 操作和 synchronized 结合实现赋值操作,多线程操作只会锁住当前操作索引的节点。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值