先看几个关键的属性
//默认数组初始化长度为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大长度
static final int MAXIMUM_CAPACITY = 1 << 30;
//负载因子,扩容的阈值,比如说16*0.75=12,当数组使用了12的时候就会触发扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当链表长度为8的时候转为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当节点小于6的时候就转为链表
static final int UNTREEIFY_THRESHOLD = 6;
//最小树形化容量阈值
static final int MIN_TREEIFY_CAPACITY = 64;
接下来看put的过程
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//hash算法,高16位跟低16位进行异或运算,这样目的是使结果更加随机性,尽可能使数据均匀分布
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
put的时候调用的是putVal方法,源码如下
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab;Node p;int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//在第一次put的时候进行初始化数组,只有使用的时候才初始化大小,体现的是懒加载的思想,也可以节省内存空间
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//数组为空的时候,直接把节点存入
tab[i] = newNode(hash, key, value, null);
else {//节点不为空的情况
Node e; K k;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;//如果key值相等,则把旧的值覆盖
else if (p instanceof TreeNode)//如果是红黑树则调用红黑树的put方法
e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);//红黑树
else {//遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//新增一个节点插入在链表最后
if (binCount >= TREEIFY_THRESHOLD - 1) // 大于等于8的时候转为红黑树,binCount是从0开始的,所以要减一
treeifyBin(tab, hash);
break;
}
//插入的key跟链表已有的key重复,则跳出循环
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;
}
}
++modCount;//修改的次数
if (++size > threshold)//当容量大于threshold,比如第一次调用resize()初始化的赋给的值12,即16*0.75
resize();//进行扩容
afterNodeInsertion(evict);
return null;
}
简单解释下put的时候主要做的操作
1、当数组table为空的时候,先调用resize()进行初始化, 根据(n - 1) & hash,这样就找到该key的存放位置
2、如果数组的key已经存在,则用新值替换旧值
3、如果当前数组节点table[i]已经存在值,则根据当前节点的类型看是红黑树还是链表,如果是红黑树则调用putTreeVal,
4、如果是链表则对链表进行循环遍历,找到末尾进行插入(尾插法)。其中链表过长还会转为红黑树
5、符合扩容条件就进行扩容
ps:每次put操作的时候返回的都是上一次插入的数据,如果节点为空,返回null,如果是覆盖操作则返回的是旧值
将链表转为红黑树
final void treeifyBin(Node[] tab, int hash) {
int n, index; Node e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();//如果数组为空或者数组的长度小于64,优先进行扩容
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode hd = null, tl = null;
do {
TreeNode p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
接下来我们看下扩容的操作,从上面分析可知扩容会发生在两个地方
1、转为红黑树的时候
2、当数组put的元素达到阈值的时候
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {//说明不是初始化
if (oldCap >= MAXIMUM_CAPACITY) {
//如果超过规定的数组最大长度,则只调整门槛值
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 向左位移一位,也就是变为原来的2倍
}
else if (oldThr > 0) //原来临界值不为空但原来容量为空的情况,则把容量设置为临界值(把原来设置的值全部删除了,这个时候oldCap==0,但是oldThr>0)
newCap = oldThr;
else {
//第一次初始化
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
}
if (newThr == 0) {//newThr为0时,按公式进行计算给newThr一个值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];//创建新的数组,这个数组第一次是16,后续就是32,64,128。。。
table = newTab;
if (oldTab != null) {//进行元素迁移
for (int j = 0; j < oldCap; ++j) {
Node e;//临时节点
if ((e = oldTab[j]) != null) {//将原来的节点赋给e,然后把原来的节点置空
oldTab[j] = null;
if (e.next == null)
//说明不是链表,计算新数组的下标,跟第一次put的时候一样
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//节点是树,按照树的方式打散
((TreeNode)e).split(this, newTab, j, oldCap);
else { //链表的方式
Node loHead = null, loTail = null;//此对象接收放在原来位置
Node hiHead = null, hiTail = null;//此对象接收会放在(原位置+原容量的值)
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {//元素位置没有发生变化
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//存放在新数组的位置,要么就是原来的位置,要么就是【原来的位置+原来数组的容量】
if (loTail != null) {
loTail.next = null;//尾节点next属性置空
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;//尾节点next属性置空
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
resize()主要做了以下几件事
1、如果是第一次初始化的时候,创建一个长度为16的数组
2、如果原数组容量不为空,则创建一个新的数组,该数组的长度为原来的2倍,阈值也为原来的2倍(都向左位移一位)
3、遍历旧数组,根据hash跟(新数组的容量-1)异或计算节点在新数组的位置,然后进行赋值
4、如果节点是红黑树就按照红黑树的方式进行存放
5、如果是链表,则存放的位置有两种可能,要么就是在原来的位置,要么就是在(原来的位置+旧数组的容量)
比如原来链表是在8,则到新的数组后要么就还是在8,要么就是8+16,这个要看(e.hash & oldCap)的结果是否为0
HashMap线程不安全的表现
通过上面的源码分析,我们可以发现HashMap线程不安全主要表现在以下几个情形:
1、如下图,如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,假设一种情况,线程A判断完毕后还未进行数据赋值时挂起,而线程B正常执行,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,这时线程A会把线程B插入的数据给覆盖,链表也类似
2、在扩容的时候,在执行赋值的时候这个时候假如有线程在赋值前插入数据,那么也是会被覆盖的,因为这个时候table已经指向了newTab了,别的线程插入的时候就是往扩容后的数组插入了
3、如果在执行扩容的时候,刚好执行到 table = newTab;这时候某个线程就立刻想删除以前插入的某个元素,你会发现删除不了,因为table指向了新数组,而这时候新数组还没有任何数据。