zoukankan      html  css  js  c++  java
  • HashMap源码解析JDK8

    一、HashMap基础

    1.1 HashMap的定义

    我们先看一下HashMap的定义:

    public class HashMap<K,V> extends AbstractMap<K,V>
            implements Map<K,V>, Cloneable, Serializable 

    1.2 HashMap的属性

    //默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    //最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //链表转成红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;
    //红黑树转为链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    //存储方式由链表转成红黑树的容量的最小阈值
    static final int MIN_TREEIFY_CAPACITY = 64;
    //HashMap中存储的键值对的数量
    transient int size;
    //扩容阈值,当size>=threshold时,就会扩容
    int threshold;
    //HashMap的加载因子
    final float loadFactor;
    

      

    二、初始化

    首先我们看无参构造函数

    HashMap<String, String> hashMap = new HashMap<>();
    

      

      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;
            this.threshold = tableSizeFor(initialCapacity);
        }
    

      这两个同样也只是赋值了加载因子和扩容阀值
    注意:tableSizeFor是的到一个二次幂的数值,其计算过程是这样的

      /**
         * Returns a power of two size for the given target capacity.
         */
        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;
        }
    

      假设初始容量为19

    cap=19
    int n = cap-1;//得到n=18,换算为二进制为0000 0000 0000 0000 0000 0000 0001 0010
    n |= n>>>1;//表示n无符号右移一位后,与n按位或计算,其中n>>>1= 1001,按位或结果为11011
    n|=n>>>2;//其中n>>>2=00110,按位或的结果为11111,下面几步类似,最终得到的结果是n=11111(二进制,也就是2^5-1,31)
    这个计算方式是想法把二进制中低位的补1,最后的值一定是2的n次幂减1
    最终计算得到的结果是32
    

      

    因为cap最大为2^31,我们可以知道,这个方法的最终目的就是返回比cap大的最小的2的幂次方。

    真正的初始化其实是在第一次put的时候,调用的是resize()方法,哪里调用resize()方法将在下面说明

    三、put()

      public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
        }

    这个hash(key)是一个比较经典的方法,这里我们看一下

        static final int hash(Object key) {
            int h;
            /**
             * 这里是hash值一个巧妙的打乱方式,目的是将hash值进一步打乱,将对象在数组中的位置划分的更为均匀
             * int 类型是32为的数值类型,并且可以区分为高16位和低16位
             * 这里的算法就是: 高16为不变,将低16位与高16为进行 异或(^/xor) 运算 两个值不相同则为1,反之则为0
             *
             */
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

    put方法调用putVal方法,这里我们看一下putVal的源码

        /**
         * Implements Map.put and related methods
         *
         * @param hash hash for key
         * @param key the key
         * @param value the value to put
         * @param onlyIfAbsent if true, don't change existing value,这个参数很少用,但如果为true,则不替换已经存在的value值。默认是替换
         * @param evict if false, the table is in creation mode.
         * @return previous value, or null if none
         */
        final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)//如果table为null则进行第一次扩容->既为第一次真正的初始化
                /**
                 * 如果table为null则进行第一次扩容->既为第一次真正的初始化
                 */
                n = (tab = resize()).length;
    
            /**
             * i = (n - 1) & hash
             * 这里是又一个巧妙的运算,通过hash值,计算该节点在数组中的下标
             * n 为当前数组的长度
             * i 为计算出的下标
             * 假如当前的长度为16(默认为HashMap的初始数组长度)
             * n 的二进制为:                   0000 0000 0000 0000 0000 0000 0001 0000
             * 则 (n-1) 为15, 在二进制看来则是 0000 0000 0000 0000 0000 0000 0000 1111
             * 此时将hash值与15进行 与(&) 运算,   &:同为1 ,则为1,否则为0
             * 无论hash值如何变化则运算之后的值一定在 【0-15】之间,则为当前节点所处的数组的下标,
             * 这里其实与 hash % 16 的概念差不多,但是这种方法更将精妙,更加高效
             */
            if ((p = tab[i = (n - 1) & hash]) == null)
                /** p 为当前数组下标下的第一个节点,如果p为null,则表示当前数组下标下还没有值,这时直接新建一个node节点放入数组下标中 **/
                tab[i] = newNode(hash, key, value, null);
            else {/** 如果当前数组下标下有值,则进行下面代码 **/
                Node<K,V> e; K k;
                /**
                 * 1. p.hash == hash
                 *  首先判断当前节点的hash值是否与将要放入节点的hash值是否一致,如果一致则继续进行判断
                 * 2. (k = p.key) == key || (key != null && key.equals(k))
                 *  这个咱们拆开看, 首先看 || 符号左边
                 *      (k = p.key) == key :这句代码首先将p.key赋值给k, 并且比较两个key值是否在同一快内存中,如果是则返回true,如果为false则继续进行判断
                 *  接下载咱们在看 || 符号右边
                 *      (key != null && key.equals(k)) : 如果key不等于null的话,调用key的equels方法判断是否一致
                 */
                if (p.hash == hash &&
                        ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p; /** 如果进入这个if里面则将p赋给临时变量e **/
                else if (p instanceof TreeNode) /** 如果p是一个红黑树,则将put对象放入红黑树中 **/
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {/** 如果进入最后的else表示key不一致,并且该数组下标下的节点也不是一个红黑树 **/
                    /**
                     * 这里将遍历该链表
                     */
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {/** 这里判断是否走到链表的最后 **/
                            /** 将新节点放入该链表的最后 **/
                            p.next = newNode(hash, key, value, null);
                            /**
                             * 这里判断是否满足将链表变成红黑树的 条件
                             *  TREEIFY_THRESHOLD 的值为8,
                             *  binCount是链表的下标,如果binCount的值为7,则代表该链表的长度为8
                             */
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                                treeifyBin(tab, hash);/** 满足条件,进行红黑树的转换 **/
                            break;
                        }
                        /**
                         * 如果没有进入链表末尾
                         * 判断当前节点的key是否与新节点的key一致,与上面if的逻辑一样,这里不在赘述。
                         */
                        if (e.hash == hash &&
                                ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;/** 如果都不满足条件,继续向下寻找 **/
                    }
                }
                /**
                 * 经过上面代码分析过后,上面情况下e不为null呢?
                 * 只要新节点的key与老节点的key相同,则e不为null, e则代表,与新节点key一直的老节点
                 */
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    /** onlyIfAbsent,putVal方法的一个参数,通过这里可以看出,该参数控制是否覆盖老值,不过前提是老节点的value不为null **/
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);/** 这里在HashMap中是一个空实现,其在LinkedHashMap中有作用,这里不讨论 **/
                    return oldValue;
                }
            }
            ++modCount; /** modCount:表示结构被修改的次数 **/
            if (++size > threshold) /** 这里我以前一直有一个误区,size表示的是map中所有节点的个数,而不是map中数组被占用的个数,这里很明确的表示出来了,如果size+1 大于阀值,则进行扩容 **/
                resize();
            afterNodeInsertion(evict);
            System.out.println(table);
            return null;
        }
    

      这里我们在看看一下resize()方法

       final Node<K,V>[] resize() {
            /** 将表格赋给临时变量 **/
            Node<K,V>[] oldTab = table;
            /**
             * oldCap: 扩容前数组大小
             * oldThr:扩容前的扩容阀值
             * 顾名思义
             * newCap:新的数组大小
             * newThr:新的扩容阀值
             */
            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;
                }
                /**
                 *  1. (newCap = oldCap << 1) < MAXIMUM_CAPACITY
                 *      新的数组大小为老数组大小右移以为,即:老数组大小乘以2
                 *  2. oldCap >= DEFAULT_INITIAL_CAPACITY
                 *      老数组大小要大于等于,默认的初始化大小即为16
                 */
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                        oldCap >= DEFAULT_INITIAL_CAPACITY)
                    /** 新的扩容阀值也扩大一杯 **/
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;/**  老的扩容阀值大于0,并将其赋值为新的数组容量,只有通过有参构造函数猜可以走到这一步 **/
            else {               // zero initial threshold signifies using defaults
                /** 当HashMap,第一次默认初始化的时候,就会走到这里 **/
                newCap = DEFAULT_INITIAL_CAPACITY; //16
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
            }
            if (newThr == 0) {/** 新的扩容阀值为0,只有通过有参构造函数猜可以走到这一步 **/
                float ft = (float)newCap * loadFactor;
                /** 判断新的大小是否小于最大数组限制,并且扩容阀值是否小于最大数组限制,否则将扩容阀值设为最大 **/
                newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                        (int)ft : Integer.MAX_VALUE);
            }
            threshold = newThr;/** 设置扩容阀值 **/
            @SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];/** 根据扩容后的容量,创建一个新的hashMap **/
            table = newTab;/** 将新的数组赋值给table属性,这里不是线程安全的 **/
            if (oldTab != null) {
                /** 循环老数组 **/
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    /**
                     * 如果老数组该下标下为null,则不管
                     * 如果不为null,则放入新数组中,并将老数组释放,交给gc去回收
                     */
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)/** 如果该链表只有一个值,则直接将其放入新数组 **/
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)/** 如果该节点是一个红黑树,则将其拆开放入新数组,和下面将链表拆成两个链表类似 **/
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { /** 如果不是红黑树,并且链表不止一个值,则进入该步骤 **/
                            /**
                             * 首先有一个基础概念,HashMap扩容都是2倍扩容,即16->32>64
                             * 那么同一个链表下的节点,在新的数组中的位置只会在两个可能的位置,
                             * 一个是原下边,一个是原下标+原来数组的长度,
                             * 至于为什么会这样?
                             * 可以类比一组数除以16和同一组数除以32.
                             *
                             * 所以:loHead,记录原下标下的接线                  loTail:记录链表末尾节点
                             *      hiHead,记录原下标+原数组长度的,下标下的节点  hiTail:记录链表末尾节点
                             */
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;/** 记录链表的下一个节点 **/
                            do {
                                next = e.next;/** 遍历下一个节点 **/
                                /**
                                 * e.hash & oldCao
                                 * 假设oldCap为16,那么二进制则是 0000 0000 0000 0000 0000 0000 0001 0000
                                 * 随便一个hash值               0000 1010 1010 1111 0100 0101 0101 1111
                                 * 如果第5位上不为1,则全部为0,表明,当前节点一定在数组的低位。如果第五位有值则表示一定 (% 32) 的余数一定比16大
                                 */
                                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;
                                newTab[j] = loHead;/** 将新的链表赋给低位的数组下标 **/
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;/** 将新的链表赋给高位的数组下标 **/
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    

      这里我们在看一下treeifyBin()方法

        /**
         * 替换给定散列的索引处的bin中的所有链接节点,除非表太小,在这种情况下调整大小。
         */
        final void treeifyBin(Node<K,V>[] tab, int hash) {
            int n, index; Node<K,V> e;
            /**
             * 这里判断是否满足转换为红黑树的最小数组大小,如果不满足,直接进行扩容操作
             */
            if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
                resize();
            /** 去需要扩容的数组下标下的链表首个节点 **/
            else if ((e = tab[index = (n - 1) & hash]) != null) {
                TreeNode<K,V> hd = null, tl = null;
                do {/** 将链表节点变为TreeNode对象,并保持链表结构 **/
                    TreeNode<K,V> 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);/** 改变为红黑树结构 **/
            }
        }
    

      

    四、 多线程不安全问题

    我们都知道,HashMap不是线程安全的。这块主要体现在两块:

    3.1 get和put

    这两块都没有加锁,所以可能会导致多线程执行时,出现数据被覆盖的问题。

    3.2 死循环的问题

    这个问题主要出现在resize的过程中,多线程都探测到需要resize时,将链表元素rehash过程中,可能会导致死循环。这个问题参考JAVA HASHMAP的死循环

    至此,源码分析基本结束。



    参考链接:https://www.jianshu.com/p/a66320235c43
  • 相关阅读:
    手把手教你利用create-nuxt-app脚手架创建NuxtJS应用
    初识NuxtJS
    webpack打包Vue应用程序流程
    用选择器代替表格列的筛选功能
    Element-UI
    Spectral Bounds for Sparse PCA: Exact and Greedy Algorithms[贪婪算法选特征]
    Sparse Principal Component Analysis via Rotation and Truncation
    Generalized Power Method for Sparse Principal Component Analysis
    Sparse Principal Component Analysis via Regularized Low Rank Matrix Approximation(Adjusted Variance)
    Truncated Power Method for Sparse Eigenvalue Problems
  • 原文地址:https://www.cnblogs.com/niunafei/p/12736214.html
Copyright © 2011-2022 走看看