HashMap及扩容
是Map接口的实现,双列集合,由<K,V>键值对组成,底层没有实现锁机制,其线程是不安全的,多线程环境下会出现数据覆盖问题,所以多线程不推荐使用HashMap,可以使用ConcurrentHashMap。
HashMap由多个Node组成,Node包括hash、key、value、next。
树化规则
HashMap里面定义了一个常量TREEIFY_THRESHOLD = 8,当链表长度超过树化阈值 8 时,先尝试调用resize()方法进行扩容来减少链表长度,如果数组容量已经 >=64(MIN_TREEIFY_CAPACITY),才会进行树化,Node节点转为TreeNode节点(TreeNode也是HashMap中定义的内部类)。
TreeNode除了Node的基本属性,还保存了父节点parent, 左孩子left,右孩子right,还有红黑树用到的red属性
扩容因子在统计学的计算下,并平衡空间利用率与时间利用率的前提下,提出为0.75,在.net开发中hashmap的负载因子为0.7
默认情况下,HashMap初始容量是16,负载因子是0.75
1.7
jdk1.7时,HashMap底层是由数组+单链表组成
此时,当数组长度大于size*负载因子且没有空位时,进行扩容,将数组的长度*2
1.8
jdk1.8时,HashMap底层得到优化,由数组+单链表+红黑树组成
jdk1.8的扩容分为两种情况:
1、当数组长度大于size*负载因子时触发扩容,新建一个两倍大小的数组
2、当数组长度还没超过阈值时,但链表长度达到8,此时,会进行扩容操作来resize,当数组大小达到64时,才会树化;数组大小达到64,HashMap就会停止扩容,而是树化来优化链表结构,提高查询效率,总之,当链表长度达到8,数组大小达到64时,才会进行树化操作
ConcurrentHashMap及扩容
1.7
在jdk1.7中是基于segment分段的,每个segment下有一个HashMap,需要扩容时(超过size*负载因子),首先在segment下新建一个容量更大(两倍)的数组,将原数组下的链表及哈希值等内容赋值到新数组中,再改变segment的指针指向新数组。
1.8
1.8版本的ConcrrentHashMap不再基于segment,需要扩容时,直接新建一个容量更大的数组Entry,此时有多个线程同时向新数组进行复制赋值,每个线程负责一定的数量,另外,当某个线程在向此集合中put时,如果发现当前正在扩容,则该线程也加入扩容任务当中。如果某个线程put时,当前集合没有正在扩容,则将key-value加入到当前集合中,计算当前是否超过容量阈值,如果超过,则扩容。
jdk1.7和1.8,HashMap发生了哪些变化
1、1.8加入了红黑树,当链表长度达到8时,就会转化为红黑树,优化了插入和查询效率
2、1.7链表中插入元素使用的是头插法,而1.8插入元素时要判断链表的长度,需要遍历链表,所以改为采用尾插法。
3、1.7中哈希算法比较复杂,存在大量右移和异或运算,因为复杂的哈希算法的目的是要提高散列性(是指对象是否具有哈希值(Hash value)以及是否可以被用作哈希表(Hash table)的键值),1.8做出了优化,新增了红黑树,适当的简化了哈希算法,节约了系统资源。
HashMap的put方法
流程如下:
- 首先根据key通过哈希算法和与运算计算要put的元素的哈希值,计算数组下标
- 如果数组下标位置元素为空,则将kv封装成Entry对象(1.7为Entry、1.8为Node),放入此位置
- 如果数组下标元素不为空,则分1.7和1.8两种情况讨论
- 对于jdk1.7,数组下标不为空,则判断该下标下的链表中是否存在该key对应的数据,如果存在,则替换掉value,若不存在,使用头插法将新元素插入到链表中。
- 对于jdk1.8,数组下标不为空,首先判断该下标下节点是链表Node还是红黑树TreeNode
- 如果是链表Node,首先将数据封装成Node对象,遍历链表,通过尾插法插入,若当前key已存在,则覆盖更新,若不存在,则插入,记录链表大小,若插入新节点后链表大小达到8,则考虑是否转化为红黑树
- 如果是红黑树Node,把kv数据封装成TreeNode节点插入到红黑树中,若key已存在,覆盖更新。
- 将kv封装成Node后插入到链表和红黑树之后再判断是否需要扩容,若不需要,则结束put。
树化源码:
private final void treeifyBin(Node<K,V>[] tab, int index) {
Node<K,V> b; int n, sc;
if (tab != null) {
//数组大小小于64,只扩容,不转化
if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
tryPresize(n << 1);
//数组大小大于64,将Node转化为TreeNode
else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
synchronized (b) {
if (tabAt(tab, index) == b) {
TreeNode<K,V> hd = null, tl = null;
for (Node<K,V> e = b; e != null; e = e.next) {
TreeNode<K,V> p =
new TreeNode<K,V>(e.hash, e.key, e.val,
null, null);
if ((p.prev = tl) == null)
hd = p;
else
tl.next = p;
tl = p;
}
//生成红黑树
setTabAt(tab, index, new TreeBin<K,V>(hd));
}
}
}
}
}
总结:
说到这里,HashMap和ConcurrentHashMap的基本数据结构和扩容机制就清晰了,这里还有一个重要的点,就是红黑树,这种数据结构十分重要,有兴趣的小伙伴可以关注我另一篇讲解红黑树的文章。