HashMap的数据结构
JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK1.8 之后
相比于之前的版本,JDK1.8 以后在解决哈希冲突时有了较大的变化。
当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()方法。这个方法会根据 HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 resize() 方法对数组扩容。
ConcurrentHashMap底层数据结构
ConcurrentHashMap
是一种线程安全的高效
Map
集合,
jdk1.7
和
1.8
也做了很
多调整。
JDK1.7
的底层采用是
分段的数组
+
链表
实现
JDK1.8
采用的数据结构跟
HashMap1.8
的结构一样,数组
+
链表
/
红黑二叉树
JDK1.7
在
jdk1.7
中
ConcurrentHashMap
里包含一个
Segment
数组。
Segment
的结构
和
HashMap
类似,是一 种数组和链表结构,一个
Segment
包含一个
HashEntry
数组,每个
HashEntry
是一个链表结构 的元素,每个
Segment
守
护着一个
HashEntry
数组里的元素,当对
HashEntry
数组的数据进行修 改
时,必须首先获得对应的
Segment
的锁。
Segment
是一种可重入的锁
ReentrantLock
,每个
Segment
守护一个
HashEntry
数组里得元 素,当对
HashEntry
数组的数据进行修改时,必须首
先获得对应的
Segment
锁
JDK1.8
在
jdk1.8
中的
ConcurrentHashMap
做了较大的优化,性能提升了不少。首先是它的数据结构与jdk1.8
的
hashMap
数据结构完全一致。其次是放弃了 Segment臃肿的设计,取而代之的是采用
Node + CAS + Synchronized
来保证并发安全进行实现,synchronized
只锁定当前链表或红黑二叉树的首节点,这样只要hash
不冲 突,就不会产生并发
,
效率得到提升
ConcurrentHashMap是如何保证线程安全的?
在JDK 1.7中,ConcurrentHashMap使用了分段锁技术,即将哈希表分成多个段,每个段拥有一个独立的锁这样可以在多个线程同时访问哈希表时,只需要锁住需要操作的那个段,而不是整个哈希表,从而提高了并发性能。
虽然JDK 1.7的这种方式可以减少锁竞争,但是在高并发场景下,仍然会出现锁竞争,从而导致性能下降。
虽然JDK 1.7的这种方式可以减少锁竞争,但是在高并发场景下,仍然会出现锁竞争,从而导致性能下降。
在JDK 1.8中,ConcurrentHashMap的实现方式进行了改进,使用分段锁(思想)和“CAS+Synchronized的机制来保证线程安全。在JDK1.8中,ConcurrentHashMap会在添加元素时,如果某个段为空,那么使用CAS操作来添加新节点;如果段不为空,使用Synchronized锁住当前段,再次尝试put。这样可以避免分段锁机制下的锁粒度太大,以及在高并发场景下,由于线程数量过多导致的锁竞争问题,提高了并发性能。