1.Map
的存储特点
在
Map
这个结构中,数据是以键值对(key-value)的形式进行存储的,每一个存储进
map
的数据都是一一对应的。
创建一个
Map
结构可以使用
new HashMap()
以及
new TreeMap()
两种方式,两者之间的区别是:
TreeMap
是支持 排序的。
2.HashMap
的底层存储方式
总结
:
1.
用
hashMap
存储数据
(key,value)
的时候使用
put
方法
2. put
方法会调用
putVal
方法
,
把
hash(hey)
和当前的
key
,
value
作为参数传进来
3.
判断数组是否为空,即判断是否是第一次添加数据
,
如果是的话,会先调用
resize
方法扩容
4.
之后
,
根据当前
key
的
hash
值找到它在数组中的下标
(
怎么算的
? index = (n - 1) & hash)
,判断当前下标位置是 否已经存在元素
5.
如果不存在,直接把
key
、
value
包装成
Node
节点作为链表头存入数组
6.
如果存在,分为三种情况
1.
)比较一下已有数据和存入数据 如果
hash
值等于传过来的
hash
,并且他们的
key
值也相等 最后会把 value的值覆盖处理
2.
)上一步不相等,就判断一下当前是不是红黑树结构,是则调用
putTreeVal()
把它加入到红黑树
3.
)既不相等,也不是红黑树结构,说明是普通链表结构,遍历这个链表,将数据存到链表尾部
1.
在遍历过程中,如果是最后一个节点,则插入新节点
newNode(hash, key, value, null)
2.
如果链表长度超过了
8
,则转化为红黑树
treeifyBin(tab, hash)
3.
如果遍历的时候遇到了相同的
key
把
value
的值覆盖处理
7.
如果当前数组中的元素个数超过阈值,则扩容
resize();
8. putVal
()方法 没修改
value
就返回
NULL
修改了就返回旧值(之前的
value
)
3.
什么是
hash
碰撞
Hash Collision
就是我们说的
Hash
碰撞或者
Hash
冲突。
这个其实也非常好理解,就是
2
个输入不同的数据,经过
Hash
算法后,得到的
Hash
值是一样的。
在
HashMap
的查询和添加过程中,绕不过去的是计算元素在数组的位置
index
,
key
的
HashCode
作为这个计算的 基础。计算后的Hash
值存在相同的情况,
hash
与长度取余的结果也有相同的情况,这个时候运算结果相同的两个 对象就需要存储到同一个链表中,这就是HashMap
中的
Hash
碰撞。
4.
如何解决
hash
碰撞
1.
开放地址方法
1
)线性探测
按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。 就是在
此空间不足时,直接放入此空间的后一个空的空间
2
)再平方探测
按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加
1
的平方个单位,若仍然存在则减
1
的平方个
单位。随之是
2
的平方,
3
的平方等等。直至不发生哈希冲突。 要注意平方不能超过容量的值
Size=16
的时候,找备
选的单元只能取
i=1,2,3
,也就是距离冲突单元
1,4,9
个单位的位置了。
3
)伪随机探测
按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值的基础上加上随机数,直至不发
生哈希冲突。
2.
链式地址法(
HashMap
的哈希冲突解决方法)
对于相同的值,使用链表进行连接。使用数组存储每一个链表。 就是
hashmap
的底层原理 :数组
+
链表 就是没有
红黑树
补充:在
JDK1.8
中
HashMap
通过链式寻址法以其红黑树来解决哈希冲突的,其中红黑树是为了优化哈希表的链表
过长 导致遍历时间复杂度增加的问题。当链表长度大于
8
并且哈希表的容量大于
64,
再向链表中添加元素
,
会转化为
红黑树。
优点:
1
)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较
2
)
由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况; (
3
)开放定址法为
减少冲突,要求装填因子
α
较小,故当结点规模较大时会浪费很多空间。而拉链法中可取
α≥
1
,且结点较大时,拉 链法中增加的指针域可忽略不计,因此节省空间; (
4
)在用拉链法构造的散列表中,删除结点的操作易于实现。
只要简单地删去链表上相应的结点即可。 缺点:
1
) 指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。
3.
建立公共溢出区
建立公共溢出区存储所有哈希冲突的数据
4.
再哈希法
对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。
5.
如何解决并发
HashMap
的线程不安全主要体现在下面两个方面:
1.
在
JDK1.7
中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.
在
JDK1.8
中,在并发执行
put
操作时会
发生数据覆盖的情况。
1
、
if((p = tab[i =(n -1)& hash])==null)// 1
、此处线程不安全
——
用来判定索引位置是否
hash
碰撞,比如两个线程
A
、
B
都在进行
put
操作,并且
hash
函数计算出的插入下标是相同的,当线程
A
执行完第六
行代码后由于时间片耗尽导致被挂起,而线程
B
得到时间片后在该下标处插入了元素,完成了正常的插入,然后线
程
A
获得时间片,由于之前已经进行了
hash
碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致 了线程B
插入的数据被线程
A
覆盖了,从而线程不安全。
2
、
if (++size > threshold)
中的
++size
:同样还是线程
A
、
B
,这两个线程同时进行
put
操作时,假设当前
HashMap
的
zise
大小为
10
,当线程
A
执行到此行代码时,从主内存中获得
size
的值为
10
后准备进行
+1
操作,但是由于时间片
耗尽只好让出
CPU
,线程
B
快乐的拿到
CPU
还是从主内存中拿到
size
的值
10
进行
+1
操作,完成了
put
操作并将
size=11
写回主内存,然后线程
A
再次拿到
CPU
并继续执行
(
此时
size
的值仍为
10)
,当执行完
put
操作后,还是将
size=11
写回内存,此时线程
A
、
B
都执行了一次
put
操作,但是
size
的值只增加了
1
,所有说还是由于数据覆盖又导
致了线程不安全。
解决方法:
1.Hashtable
HashTable
为了实现多线程安全,在几乎所有的方法上都加上了
synchronized
锁(锁的是类的实例
,
也就是整个
map
结构),当一个线程访 问
Hashtable
的同步方法时,其他线程如果也要访问同步方法,会被阻塞住。
2.Collections.synchronizedMap(
一般不用
)
缺点
:
从锁的角度来看,基本上是锁住了尽可能大的代码块
.
性能会比较
差
3.ConcurrentHashMap
(常用)
JDK 1.7
中,采用分段锁的机制,实现并发的更新操作,底层采用数组
+
链表的 存储结构,包括两个核心静态内部类 Segment
和
HashEntry
。 ①、
Segment
继承
ReentrantLock
(重入锁) 用
来充当锁的角色,每个
Segment
对象守护每个散列映射表的若干个桶; ②、
HashEntry
用来封装映射表的键
-
值 对; ③、每个桶是由若干个 HashEntry
对象链接起来的链表 分段锁:
Segment
数组中,一个
Segment
对象就是一 把锁,
对应一个
HashEntry
数组
,
该数组中的数据同步依赖于同一把锁
,
不同
HashEntry
数组的读写互不干扰
JDK 1.8
中抛弃了原有的
Segment
分段锁,来保证采用
Node + CAS + Synchronized
来保证并发安全性。取消类 Segment,直接用
table
数组存储键值对;当
Node
对象组成的链表长度超过
TREEIFY_THRESHOLD
时,链表转换
为红黑树,提升性能。底层变更为数组
+
链表
+
红黑树。
CAS
性能很高,但
synchronized
之前一直都是重量级的 锁,jdk1.8
引入了
synchronized
,采用锁升级的方式。