HashMap
HashMap是数组(桶)+链表+红黑树(JDK1.8后,若链表长度大于8时,则转化为红黑树)
final int hash; //用来定位数组索引位置 final K key; V value; Node<K,V> next; //链表的下一个node
Node是HashMap的一个内部类,本质是就是一个映射(键值对),HashMap使用哈希表来存储的。HashMap采用了链地址法解决冲突。
int threshold; // 所能容纳的key-value对极限 final float loadFactor; // 负载因子 int modCount; int size;
首先,Node[] table的初始化长度length(默认值是16),Load factor为负载因子(默认值是0.75),threshold(阈值)是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。key为null的Node在table[0]位置,value也可为null。
下面为hash()方法和indexFor()方法的源码解析,第二步中h ^ (h >>> 16)的原因是,因为table的length一般比较小,只需要考虑到较低的16位即可,提高运算的效率。关于第三步其实是取余运算,因为Node[] table的length长度总为2的幂,h%length等价于 h&(length-1),但是位运算比取余运算快得多。
static final int hash(Object key) { //jdk1.8 & jdk1.7 int h; // h = key.hashCode() 为第一步 取hashCode值 // h ^ (h >>> 16) 为第二步 高位参与运算 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } static int indexFor(int h, int length) { //jdk1.7的源码,jdk1.8没有这个方法,但是实现原理一样的 return h & (length-1); //第三步 取模运算 }
扩容机制
即 resize() 方法,Java 中,数组的长度是固定的,当 HashMap 中的键值对数量超过阈值时,进行扩容,长度变为原来的两倍,要重新计算键值对的位置,并把它们移动到合适的位置上去。HashMap线程不安全,多线程调用resize时,会出现死循环,链表产生环。下面为产生死循环讲解
图上面为线程1进行resize方法,正处理元素5(e)时,下一个元素指向9(next),线程时间片用完,或其他原因导师线程暂停,线程2直接执行完resize方法。
线程1被唤醒,继续执行,table[1]的第一元素指向5,其next指向元素9。
线程1继续执行,处理完5后,处理9,而此时9的next有指向5,即出现循环链表,这时table[1]的第一个元素指向9,处理9后,继续处理5,5就会为这时table[1]首个Node,此时的next为9,9的next为5,出现死循环。