前言
HashMap是我们日常工作中接触较多的集合之一,HashMap是线程不安全的,多线程环境下,建议使用ConcurrentHashMap。
此处想到一个面试题,问:HashMap是怎么表现其线程不安全的?当时我差点就懵逼了,仔细想了一下,不安全?那肯定和多线程有关系!答:只有一个线程的时候HashMap是安全的,但是在多线程情景下,HashMap作为共享变量时是线程不安全。
面试必问问题HashMap底层原理,以下内容是个人对JDK8下HashMap的扩容机制的见解,如有错误,敬请指正。
红黑树过于复杂,本文将略过红黑树部分。奔着红黑树来的,可以移步了,对不起 。
扩容机制
红黑树过于复杂,不进行详细研究。
//说明:oldCap=旧桶长度,oldThr=旧桶阈值(数组长度*加载因子)
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//旧数组长度>0
if (oldCap > 0) {
//判断旧桶长度是否≥最大允许长度(1<<30),如果是就让threshold=1<<31,并返回旧数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}//给新桶长度赋值为旧桶长度*2,并判断是否<最大允许长度(1<<30),并且大于等于1<<4(16),如果是,则newThr=oldThr*2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
//以下旧桶长度=0的场景
//旧桶阈值>0,那么新桶长度=旧桶阈值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//旧桶长度=0 旧桶阈值=0,初始化newCap=16 newThr=12
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//上面都走完。newThr如果为0,则重新计算新容器阈值(新桶长度*加载因子)
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
//如果旧桶不是空的,则要进行数据迁移
if (oldTab != null) {
//遍历旧桶
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
//如果旧桶根节点不是空的,则置为空
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//判断是否有next节点,如果没有则重新计算在新桶中的下标
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果有next节点,判断是否是红黑树节点
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//以下是链表节点的情况
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//判断该节点的hash值和旧桶长度进行与运算是否为0,
//如果是0,
if ((e.hash & oldCap) == 0) {
//判断loTail是否为空,如果为空则将该节点放入loHead,否则loTail.next = e,最后将e放入loTail
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//如果不是0
else {
//判断hiTail是否为空,如果为空则将该节点放入hiHead,否则hiTail.next = e,最后将e放入hiTail
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//如果loTail不为空,则将loHead数据放入新桶下标相同的位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//如果hiTail不为空,则将hiHead数据放入新桶下标为在旧桶中下标+旧桶长度的位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
总结
1.当新加入一个元素后,若map的size大于阈值时,进行扩容。
2.扩容机制:获取map中的元素,如果元素存在数组上,则根据新数组长度重新计算下标(hash & (n-1)),并保存;
如果元素存在链表上,则将key的hash值和旧数组长度进行与运算(e.hash & oldCap),如果为0,则放入新数组对应下标j的位置;
如果不为0,则放入新数组下标为j+oldCap(旧下标+旧数组长度)位置。