HashMap PUT过程 扩容死循环

HashMap扩容过程


一、开始分析

JDK版本基于JDK1.8以上,JDK1.8以上的HashMap使用红黑树+链表的组合方式
//调用putVal方法,传入key,value
public V put(K key, V value) {
   return putVal(hash(key), key, value, false, true);
}

二、分析过程

先看官方说明

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key  存储的key的hash值
     * @param key the key  存储的键值
     * @param value the value to put  存储的值
     * @param onlyIfAbsent if true, don't change existing value
     * //判断是否需要替换旧值,每次插入重复的key,替换为最新的value
     * @param evict if false, the table is in creation mode.
     * //如果为false代表还在被创建
     * @return previous value, or null if none
     */

接下来看代码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //这里定义了四个参数
        // tab 定义一个空的Node 哈希桶数组 
        // n : 桶的长度
        // p:存储在当前位置的元素
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //table:hashmap中的重要的组成部分 transient Node<K,V>[] table; 
        //初始化HashMap
        if ((tab = table) == null || (n = tab.length) == 0)
            //一边初始化一边给n赋值
            n = (tab = resize()).length;
        //tab[0] 这种写法很好理解嘛,就是看hash位置有没有元素
        if ((p = tab[i = (n - 1) & hash]) == null)
            //将数据插入进去,数组的常规赋值方式
            tab[i] = newNode(hash, key, value, null);
        else {
        //刚才if是没有元素,else 很显然,有元素了,这个时候需要链表或者红黑树了
            Node<K,V> e; K k;
            //判断当前key是否相等
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//直接替换
            //判断p是不是一个树节点,是树了,就说明这个数据存储在红黑树上面
            else if (p instanceof TreeNode)
                //调用putTreeVal的方式将值添加到树里面
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
            //key没有,也不是红黑树,这个时候就是链表了
                //一直for循环
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //一直向后找,找节点的地方为空的地方
                        p.next = newNode(hash, key, value, null);
                        //如果找到第七个都没找到,那么就要使用红黑树了
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        //退出循环
                        break;
                    }
                    //找到了就停下来
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // e 是被替换出来的值,这个时候就替换值
            if (e != null) { // existing mapping for key
                //把当前的值给旧的值
                V oldValue = e.value;
                //看是不是需要替换值,不需要还把旧的值返回给你
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //空方法,允许自己去实现插入之后的一些操作
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //size太大了,扩容
        if (++size > threshold)
            resize();
        //空实现
        afterNodeInsertion(evict);
        return null;
    }

多线程扩容死循环

HashMap扩容死循环只在JDK1.8之前出现,为什么呢,因为使用头插法导致的呀,JDK 1.8 之后使用尾插法解决了这个问题。
所以JDK1.7在多线程的情况下为什么会出现死循环呢?
采用头插法有个坏处,就是插入之后,元素的位置倒过来了
假设原元素的顺序是 1-2 , 扩容后顺序是 2-1

1,先看看jdk1.7扩容的时候怎么弄的

void transfer(Entry[] newTable) {
	Entry[] src = table;
	int newCapacity = newTable.length;
	for (int j = 0; j < src.length; j++) {
		Entry<K,V> e = src[j];
		if (e != null) {
			src[j] = null;
			do {
				Entry<K, V> next = e.next;
				int i = indexFor(e.hash, newCapacity);	-----------------1
				//使用头插法将需要转移的节点插入到hash桶中原有的单链表中
				e.next = newTable[i];					-----------------2
				//将hash桶的指针指向单链表的头节点
				newTable[i] = e;						-----------------3
				//转移下一个需要转移的节点
				e = next;
			} while (e != null);
		}
	}
}

假设在多线程的情况下,去进行扩容
假设有两个线程 T1 和 T2
T1 在执行到e = e.next 的时候挂起,下一步就是T2去执行
T2 执行完了 把原有的 1 2 3 的顺序改成了 3 2 1
那么T1 再去执行的时候 , 发现原本喔的next 原本是 null 的 现在有值了,
那么一直next一直都会找到值,那么就造成死循环了。
说白了,就是扩容的过程中,有另外的线程把数组的next给改了
就是你要去洗头,别人把你洗发水换成沐浴露了,头永远洗不干净是一个道理。

那怎么解决死循环呢

1 升级JDK版本
2 使用HashTable
3 使用ConcurrentHashTable
4 使用Collections 返回一个线程安全的HashMap

总结

以上就是HashMap的put过程, 可以总结为一句话 首先先初始化一个HashMap,判断当前计算出来的Hash值在数组中是否已经存在,如果不存在,直接插入,如果存在,先判断key以及hash值是否相同,如果相同替换旧的值,如果不同,就先判断当前元素是不是树节点,如果是直接添加到树中,如果不是红黑树,那么就是链表,启动一个无限的for循环向后查找有没有,如果有找到有空座位的地方,直接插入,如果一直没有,当找到第七个的时候还没找到就转为红黑树,然后承接上一步,替换旧值,并且有可以自己手动实现插入后的方法,put完毕。
上一篇:Linux CentOS 配置Yaf框架


下一篇:HashMap 原理源码分析