HashMap底层源码透彻解析

HashMap源码解析

HashMap简介

HashMap在底层数据结构上采用了数组+链表+红黑树,通过散列映射来存储键值对数据因为在查询上使用散列码(通过键生成一个数字作为数组下标,这个数字就是hashcode)所以在查询上的访问速度比较快,HashMap最多允许一对键值对的Key为Null,允许多对键值对的value为Null。它的线程不是安全的,在排序上面是无序的。

img

HashMap继承关系

HashMap底层源码透彻解析_第1张图片

HashMap主要成员变量

    	//Node可以看做就是一个节点,多个Node节点构成链表,当链表长度大于8的时候转换为红黑树。
    	static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            Node<K,V> next;
        //默认初始容量
        static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
        //最大容量
        static final int MAXIMUM_CAPACITY = 1 << 30;
        //默认负载因子
        static final float DEFAULT_LOAD_FACTOR = 0.75f;
    	//树形化阈值,当链表节点个大于等于TREEIFY_THRESHOLD - 1时,会将该链表换成红黑树
        static final int TREEIFY_THRESHOLD = 8;
    	//解除树形化阈值,当链表节点小于等于这个值时,会将红黑树转换成普通的链表
        static final int UNTREEIFY_THRESHOLD = 6;
    	//最小树形化的容量,即:当内部数组长度小于64时,不会将链表转化成红黑树,而是优先扩充数组
        static final int MIN_TREEIFY_CAPACITY = 64;
        //hashmap的内部数组,Node则是链表节点对象
        transient Node<K,V>[] table;
        //容器类成员
        transient Set<Map.Entry<K,V>> entrySet;
    	//元素个数
        transient int size;
    	//容器结构的修改次数
        transient int modCount;
    	//阈值,超过这个值时扩充数组。threshold = capacity * load factor
        int threshold;
    	//负载因子
        final float loadFactor;

HashMap的源码分析

    	//默认数组初始容量为16,负载因子为0.75f
    	public HashMap() {
            this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
        }
    	
    	//指定具体的初始容量
        public HashMap(int initialCapacity) {
            this(initialCapacity, DEFAULT_LOAD_FACTOR);
        }
    	//指定具体的初始容量和负载因子
        public HashMap(int initialCapacity, float loadFactor) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal initial capacity: " +
                                                   initialCapacity);
            if (initialCapacity > MAXIMUM_CAPACITY)
                initialCapacity = MAXIMUM_CAPACITY;
            if (loadFactor <= 0 || Float.isNaN(loadFactor))
                throw new IllegalArgumentException("Illegal load factor: " +
                                                   loadFactor);
            this.loadFactor = loadFactor;
            //tableSizeFor方法返回一个比给定整数大且最接近的2的幂次方整数
            this.threshold = tableSizeFor(initialCapacity);
        }
    
    	//进入tableSizeFor方法
        static final int tableSizeFor(int cap) {
            int n = cap - 1;
            n |= n >>> 1;
            n |= n >>> 2;
            n |= n >>> 4;
            n |= n >>> 8;
            n |= n >>> 16;
            return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
        }
    
           /**原理实际上就是补位,将原本为0的空位填补为1,最后加1时,最高有效位进1,其余变为0
            * int n = cat - 1就是防止cap已经是2的幂,执行后面无符号操作时,返回的capacity是这个cap		    * 的两倍。
           */
    		
    		//假设传进来的cap值是10,n = cap -1 = 9
    		// 0000 1001 >>> 1 右移运算为 0000 0100 最后与运算 0000 1101
    		// 0000 1101 >>> 2 右移运算为 0000 0011 最后与运算 0000 1111
    		// 0000 1111 >>> 4 右移运算为 0000 0000 最后与运算 0000 1111
    		// 0000 1111 >>> 8 右移运算为 0000 0000 最后与运算 0000 1111
    		// 0000 1111 >>> 16 右移运算为 0000 0000 最后与运算 0000 1111
    		// n = n + 1 = 16
    		
    		//得到的这个capacity却赋值给了threshold。但按实际应该这么写:this.threshold = tableSizeFor(initialCapacity) * this.loadFactor;但是在构造方法中,并没有对table这个成员变量进行初始化,反而被推迟到了put方法中,在put方法中对threshold重新计算。
    	//构造一个和指定map有相同mappings的HashMap
        public HashMap(Map<? extends K, ? extends V> m) {
            this.loadFactor = DEFAULT_LOAD_FACTOR;
            //将m中所有的元素存入HashMap中
            putMapEntries(m, false);
        }
    	
    	//进入putMapEntries方法
        final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
            //获取m中元素的个数
            int s = m.size();
            //当m中有元素时
            if (s > 0) {
                //判断table是否已经初始化,如果为初始化则先初始化一些变量
                if (table == null) { // pre-size
                    //得到最小应设置的容量
                    float ft = ((float)s / loadFactor) + 1.0F;
                    //判断是否超过最大容量
                    int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                             (int)ft : MAXIMUM_CAPACITY);
                    //把要创建的HashMap的容量存放到threshold中
                    if (t > threshold)
                        threshold = tableSizeFor(t);
                }
                //如果已经初始化,判断map的size是否大于threshold,如果大于则进行resize()方法扩容
                else if (s > threshold)
                    resize();
                //遍历map中的元素
                for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                    K key = e.getKey();
                    V value = e.getValue();
                    //元素插入
                    putVal(hash(key), key, value, false, evict);
                }
            }
        }
        //进入扩容方法resize
        final Node<K,V>[] resize() {
            //保存当前table
            Node<K,V>[] oldTab = table;
            //保存当前table的容量
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            //保存当前阈值
            int oldThr = threshold;
            //定义新的table的容量和阈值
            int newCap, newThr = 0;
            //判断原table的容量是否大于0,若大于0则代表原来的table表非空
            if (oldCap > 0) {
                //若旧table的容量大于最大容量,更新阈值为Integer.MAX_VALUE,这样以后不会自动扩容
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                //容量翻倍,使用左移效率更高
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    //阈值翻倍
                    newThr = oldThr << 1; // double threshold
            }
            //如果数组还没创建,但是已经指定了threshold(这种情况是带参构造创建的对象),threshold的值为数组长度
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            //这种情况是通过无参构造创建的对象
            else {               // zero initial threshold signifies using defaults
                newCap = DEFAULT_INITIAL_CAPACITY;
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            //可能是上面newThr = oldThr << 1时,最高位被移除了,变为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"})
            //初始化table
                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;
                        //如果链表只有一个节点,那么直接重新计算索引存入新数组
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        //如果该节点是红黑树,执行split方法,和链表类似的处理
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        //此时节点是链表
                        else { // preserve order
                            // loHead,loTail为原链表的节点,索引不变
                            Node<K,V> loHead = null, loTail = null;
                            // hiHeadm, hiTail为新链表节点,原索引 + 原数组长度
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            // 遍历链表
                            do {
                                next = e.next;
                                //最高位==0,这是索引不变的链表
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                //最高位==1,这是索引发生改变的链表
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            // 原链表存回原索引位
                            if (loTail != null) {
                                // 链表最后得有个null
                                loTail.next = null;
                                // 链表头指针放在新桶的相同下标(j)处
                                newTab[j] = loHead;
                            }
                            //新链表存到:原索引位 + 原数组长度
                            if (hiTail != null) {
                                hiTail.next = null;
                                //后节点新的位置一定为原来基础上加上oldCap
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }

HashMap底层源码透彻解析_第2张图片

    	//元素插入putval方法
    	// onlyIfAbsent:当存入键值对时,如果该key已存在,是否覆盖它的value。false为覆盖,true为不覆盖。
    	// evict:用于子类LinkedHashMap。
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
           	// tab:内部数组
        	// p:hash对应的索引位中的首节点
        	// n:内部数组的长度
        	// i:hash对应的索引位
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            // 首次put时,内部数组为空,扩充数组。
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            // 计算数组索引,获取该索引位置的首节点,如果为null,添加一个新的节点
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            else {
                Node<K,V> e; K k;
                // 如果首节点的key和要存入的key相同,那么直接覆盖value的值
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                // 如果首节点是红黑树的,将键值对插添加到红黑树
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            	// 此时首节点为链表,如果链表中存在该键值对,直接覆盖value。
            	// 如果不存在,则在末端插入键值对。然后判断链表是否大于等于7,尝试转换成红黑树。
            	// 注意此处使用“尝试”,因为在treeifyBin方法中还会判断当前数组容量是否到达64,
            	// 否则会放弃次此转换,优先扩充数组容量。
                else {
                    // 走到这里,hash碰撞了。检查链表中是否包含key,或将键值对添加到链
                    for (int binCount = 0; ; ++binCount) {
                        // p.next == null,到达链表末尾,添加新节点,如果长度足够,转换成树结构。
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);
                            break;
                        }
                        // 检查链表中是否已经包含key
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                // 覆盖value的方法。
                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)
                resize();
            afterNodeInsertion(evict);
            return null;
        }

总结来说就是以下几个步骤:

1.检查数组是否为空,执行resize()扩充;
2.通过hash值计算数组索引,获取该索引位的首节点。
3.如果首节点为null,直接添加节点到该索引位。
4.如果首节点不为null,那么有3种情况
① key和首节点的key相同,覆盖value;否则执行②或③
② 如果首节点是红黑树节点(TreeNode),将键值对添加到红黑树。
③ 如果首节点是链表,将键值对添加到链表。添加之后会判断链表长度是否到达TREEIFY_THRESHOLD - 1这个阈值,“尝试”将链表转换成红黑树。
5.最后判断当前元素个数是否大于threshold,扩充数组。

    	static final int hash(Object key) {
        	int h;
        	return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    	}
    	
    	//hash方法的作用是将hashCode进一步的混淆,增加其“随机度”,试图减少插入hashmap时的hash冲突,换句更专业的话来说就是提高离散性能。
    	
    	//在putVal方法中(不仅仅只在putVal中),有这么一行代码
    	if ((p = tab[i = (n - 1) & hash]) == null)
            	tab[i] = newNode(hash, key, value, null);
    	//i = (n - 1) & hash,n是数组长度,hash就是通过hash()方法进行高低位异或运算得出来的hash值。这个表达式就是hash值的取模运算,上面已经说过当除数为2的次方时,可以用与运算提高性能。那么我们想想,大多数情况下,内部数组的容量一般都不会很大,基本分布在16~256之间。所以一个32位的hashCode,一直都用最低的4到8位进行与运算,而高位几乎没有参与。所以通过hash()方法,将hashCode高16位与低16位进行异或运算,能有效的提高离散性能。

JDK1.7和1.8HashMap的不同点

  1. 最重要的一点是底层结构不一样,1.7是数组+链表,1.8则是数组+链表+红黑树结构;
  2. jdk1.7中当哈希表为空时,会先调用inflateTable()初始化一个数组;而1.8则是直接调用resize()扩容;
  3. 插入键值对的put方法的区别,1.8中会将节点插入到链表尾部,而1.7中是采用头插;
  4. jdk1.7中的hash函数对哈希值的计算直接使用key的hashCode值,而1.8中则是采用key的hashCode异或上key的hashCode进行无符号右移16位的结果,避免了只靠低位数据来计算哈希时导致的冲突,计算结果由高低位结合决定,使元素分布更均匀;
  5. 扩容时1.8会保持原链表的顺序,而1.7会颠倒链表的顺序;而且1.8是在元素插入后检测是否需要扩容,1.7则是在元素插入前;
  6. jdk1.8是扩容时通过hash&cap==0将链表分散,无需改变hash值,而1.7是通过更新hashSeed来修改hash值达到分散的目的;
  7. 扩容策略:1.7中是只要不小于阈值就直接扩容2倍;而1.8的扩容策略会更优化,当数组容量未达到64时,以2倍进行扩容,超过64之后若桶中元素个数不小于7就将链表转换为红黑树,但如果红黑树中的元素个数小于6就会还原为链表,当红黑树中元素不小于32的时候才会再次扩容。

HashMap总结

HashMap为什么是线程不安全的

1、put的时候导致的多线程数据不一致。
这个问题比较好想象,比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)。

HashMap底层源码透彻解析_第3张图片

我们假设有两个线程同时需要执行resize操作,我们原来的桶数量为2,记录数为3,需要resize桶到4,原来的记录分别为:[3,A],[7,B],[5,C],在原来的map里面,我们发现这三个entry都落到了第二个桶里面。
假设线程thread1执行到了transfer方法的Entry next = e.next这一句,然后时间片用完了,此时的e = [3,A], next = [7,B]。线程thread2被调度执行并且顺利完成了resize操作,需要注意的是,此时的[7,B]的next为[3,A]。此时线程thread1重新被调度运行,此时的thread1持有的引用是已经被thread2 resize之后的结果。线程thread1首先将[3,A]迁移到新的数组上,然后再处理[7,B],而[7,B]被链接到了[3,A]的后面,处理完[7,B]之后,就需要处理[7,B]的next了啊,而通过thread2的resize之后,[7,B]的next变为了[3,A],此时,[3,A]和[7,B]形成了环形链表,在get的时候,如果get的key的桶索引和[3,A]和[7,B]一样,那么就会陷入死循环。

如果在取链表的时候从头开始取(现在是从尾部开始取)的话,则可以保证节点之间的顺序,那样就不存在这样的问题了。

HashMap的put方法

HashMap底层源码透彻解析_第4张图片

你可能感兴趣的:(Java源码)