HashMap1.8插入元素、扩容部分源码分析以及线程不安全的原因

先看几个关键的属性

//默认数组初始化长度为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,如果是覆盖操作则返回的是旧值
HashMap1.8插入元素、扩容部分源码分析以及线程不安全的原因_第1张图片

将链表转为红黑树

    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插入的数据给覆盖,链表也类似
HashMap1.8插入元素、扩容部分源码分析以及线程不安全的原因_第2张图片
2、在扩容的时候,在执行赋值的时候这个时候假如有线程在赋值前插入数据,那么也是会被覆盖的,因为这个时候table已经指向了newTab了,别的线程插入的时候就是往扩容后的数组插入了
HashMap1.8插入元素、扩容部分源码分析以及线程不安全的原因_第3张图片
HashMap1.8插入元素、扩容部分源码分析以及线程不安全的原因_第4张图片

3、如果在执行扩容的时候,刚好执行到 table = newTab;这时候某个线程就立刻想删除以前插入的某个元素,你会发现删除不了,因为table指向了新数组,而这时候新数组还没有任何数据。
HashMap1.8插入元素、扩容部分源码分析以及线程不安全的原因_第5张图片

你可能感兴趣的:(JAVA基础)