网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
- 首先理解什么是hash碰撞问题,HashMap存放的元素的KEY需要经过hash计算得到hash值,而这个hash值可以理解为就是此元素要存放的位置,即数组中的下标;但是如果两个不同元素经过hash计算后,得到的hash值相同时,即两个元素要存放的位置为同一个位置,产生冲突,这种现象就叫做hash碰撞。
- 要想了解HashMap是如何解决hash碰撞的,那我们就需要看看HashMap的put方法的源码中的核心操作
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//添加第一个元素时,会进入这个if结构,table为null,则第一次初始化这个table数组的长度为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//判断添加的元素的KEY经过hash计算得到的下标位置是否为null
if ((p = tab[i = (n - 1) & hash]) == null)
//如果是null,则直接添加元素
tab[i] = newNode(hash, key, value, null);
//不为null的情况
else {
Node<K,V> e; K k;
//如果key相同,则用新的元素添加到旧元素的位置,后续会将就元素返回
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//判断是否为红黑树节点,如果是红黑树节点,则添加为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//链表类型
else {
//通过for循环遍历链表节点
for (int binCount = 0; ; ++binCount) {
//如果链表节点next节点为空
if ((e = p.next) == null) {
//则添加至链表的next节点属性中
p.next = newNode(hash, key, value, null);
//如果链表节点 >= 7 说明链表存在8个已存的元素节点
if (binCount >= TREEIFY\_THRESHOLD - 1) // -1 for 1st
//转为红黑树方法
treeifyBin(tab, hash);
break;
}
//如果KEY相同,匹配其他API 如 putIfAbsent()
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//存入新值,返回旧值
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
....
当链表节点的数量达到8个时,通过treeify转为红黑树
总结:HashMap是通过3层 if 结构来判断,数组下标位置是否有元素和下标位置的类型是链表还是红黑树,然后通过链表和红黑树来解决hash碰撞的问题,当链表节点>=7时(当链表节点的数量达到8个时),会通过treeify转为红黑树。
HashMap的扩容机制?HashMap的数组何时需要扩容?
1.首次添加元素
HashMap在第一次添加元素时,会进入第一个if结构来初始化数组的长度
2.初始容量16
//添加第一个元素时,会进入这个if结构,table为null,则第一次初始化这个table数组的长度为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
3.大于16时,双倍扩容
此处resize方法就是扩容方法,jdk8中,resize方法除了扩容还增加了初始化的功能,进入此方法我们可以看一下源码
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//如果当前数组的长度>=最大值(2^30)时,将预值threshold设置为最大值
if (oldCap >= MAXIMUM\_CAPACITY) {
threshold = Integer.MAX\_VALUE;
return oldTab;
}
//如果当前数组的长度>=默认的初始长度16,则双倍扩容
else if ((newCap = oldCap << 1) < MAXIMUM\_CAPACITY &&
oldCap >= DEFAULT\_INITIAL\_CAPACITY)
newThr = oldThr << 1; // double threshold
}
...
调用resize方法的地方
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
...
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
总结分析
- 从上面可以看出,HashMap在完成put元素存储后,会判断++size是否>了阈值,如果是就会去扩容
- 下面这个方法是在,put元素为链表节点,并且要转为红黑树时,会调用该方法,该方法会在一开始就判断是否需要扩容
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)
resize();
- 判断扩容的核心就是threshold这个值
- 从resize方法中看到,HashMap在扩容时,是之前的双倍扩容
HashMap设置长度为11,那么数组的容量为多少?
第一个2的幂次方的值
指定了长度初始化HashMap时,它会将数组的容量经过一系列算法,设置为大于我们自定义值的第一个2的幂次方的值,
即 设置为11 , 则为2^4=16
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;
}
HashMap 何时从红黑树转换为 单链模式?
- HashMap在resize方法执行时,会将元素从旧数组转入新数组,此时如果转移元素为红黑树结构,那么就会调用split方法来分割红黑树方便转移
- split方法内部,在分割时,会生成高位树与低位树两种,此时也会进行判断如果红黑树元素的高位或者低位节点个数<6时,那么就调用untreeify方法来退回链表结构
split方法和untreeify方法
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
... //lowhead 低位树
if (loHead != null) {
//在红黑树节点元素往新数组中添加时,会调用split方法来重组这个红黑树
//此时会判断,红黑树的节点操作次数是否<6,即low树(低位树的节点数)< 6时,会通过untreeify方法来退化为链表
if (lc <= UNTREEIFY\_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
//此时会判断,红黑树的节点操作次数是否<6,即high树(高位树的节点数)< 6时,会通过untreeify方法来退化为链表
//highhead 高位树
if (hiHead != null) {
if (hc <= UNTREEIFY\_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
}
HashMap 为什么在多线程并发使用过程中,容易造成死循环/死锁?
图示:
- 产生死循环的核心原因是因为,jdk1.7采用的是头插法,即新来元素在链表起始的位置,而jdk1.8采用尾插法,可以有效的避免在多线程操作中产生以上死循环
- 但是HashMap不是线程安全的,所以在多线程的场景中,虽然不会出现死锁/死循环,但是还是会出现节点丢失的情况,所以在并发的场景中推荐使用ConcurrentHashMap
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
-ZgazxFlV-1715271155359)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!