本人作为掘金社区的一个小萌新,在翻看各路大神写的技术博客时,佩服其知识储备之丰富,羡慕其文笔之潇洒,于是萌生了俺也试试的想法。鉴于初次撰笔,若有不足,烦请各位看官多多见谅,在评论区留下您宝贵的意见,不胜感激。
好了,废话不多说,直接进入今天的主题。。。。
HashMap
1)存储键值对(key-value)类型的数据
2)线程不安全
3)jdk1.2出现
在jdk1.7及之前HashMap使用的是数组+链表来实现的,在jdk1.8之后增加了一个红黑树的结构。
Node对象作为HashMap存储数据的一个基本单元,它里面的内容大概包括:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; //hash值
final K key;
V value;
Node<K,V> next;//指向下一个结点
所以这个hash值是个什么东西?
/*
散列函数
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
所谓的hash值不过是将key的hashCode和它自身的高十六位做异或运算(相同为0,不同为1)罢了,这样做的好处是让key的高位也参与到运算中,从而使得到的hash值更加散列,减少哈希冲突。
在HashMap的寻址过程中首先经过以上散列函数计算得到hash值,再将hash值和table.length -1 做与运算(只有同为1,才为1)得到在数组中存放的索引。
index = hash & (table.length -1)
等价于:hash % table.length
但是&运算比%运算效率更高,所以选择&
在HashMap中数组table的长度为什么一定是2的幂次方?
/*
计算数组长度的函数
*/
static final int tableSizeFor(int cap) {
int n = cap - 1; //减一是为了防止最后返回的结果比我们预期扩大了一倍的结果
//以下是一系列的位运算
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
上述方法返回的结果一定是比传入的cap大且离它最近的一个2的幂次方的一个数,不信的小伙伴可以自己动手计算一下。。
链表何时转红黑树的?是不是像传闻中所说达到树化阈值8就转为红黑树?
/*
树化函数
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index;
Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) //64
resize();
else if...
在源码中可以看到,当链表节点数满8触发树化操作后,首先是进行一个判断(上述代码),只有在数组长度满MIN_TREEIFY_CAPACITY(最小树化容量64),才会进行树化,否则会进行扩容而不是树化。
为什么TREEIFY_THRESHOLD = 8 ,UNTREEIFY_THRESHOLD = 6 ?
红黑树的查找效率不是比链表高吗?为什么不干脆放弃链表结构?我觉得应该是在查找效率和转树结构的时间上做的一个权衡吧,太早转成树花费时间太多得不偿失,超过8继续使用链表又太拉胯,选择6和8,中间还有个差值7可以有效防止链表和树频繁转换,假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
put操作:
①.判断键值对数组table[i]是否为空(长度为0)或为null,否则执行resize()进行扩容
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可
⑥.插入成功后,判断实际存在的键值对数量size是否超扩容阈值threshold,如果超过,执行resize()扩容
ps:在jdk1.7及之前,使用的是头插法,弊端:在转移数据、扩容后,容易出现链表逆序的情况,在高并发put的情况下,可能会出现环链,下次遍历会造成死循环;jdk8之后改用的尾插法,能避免这种情况。
resize():
触发条件:1.第一次进行put操作时 2.当元素数量达到扩容阈值(capacity * loadFactor)
过程: 1.判断,极端条件下(数组长度已达最大值 MAXIMUM_CAPACITY = 2^31 -1),则不再扩容 2.创建一个长度是原数组长度两倍的newTable。 3.保存旧数组 4.重新计算每个数据在新数组中的位置 5.将旧数组上的数字转移到新的数组中 6.重新设置扩容阈值。
ps:关于负载因子loadFactor,默认等于0.75,可以更改,但是不建议改,毕竟是官方给出的最优解。。。改大了,该扩容的时候不扩容,增加哈希冲突的次数;改小了,又会频繁扩容损失性能。
怎样让HashMap变得线程安全?
1.改用Hashtable,hahaha。。。开玩笑,这位老兄每个方法都用synchronized无脑加锁,效率实在太低。
2.使用Collections类的synchronizedMap方法返回一个线程安全的map。
3.改用ConcurrentHashMap(推荐)