zoukankan      html  css  js  c++  java
  • HashMap源码学习

    HashMap实现了Map接口,继承自AbstractMap,并且是LinkedHashMap的父类。

    JDK8中的HashMap

    在jdk8中,HashMap的底层的存储结构是一个Node对象的数组,也叫哈希桶,每个桶放的是链表,链表中的元素,就是HashMap中的元素。

    涉及到扩容,关于扩容的参数有:

    • initialCapacity(初始容量),loadFactor(负载因子),threshold(阈值,等于数组的长度乘以loadFactor

    JDK8中,当链表长度达到8时,会转化成红黑树。

    1.链表节点Node

    与JDK1.7中的HashMap不同,1.8中的链表节点类名是Node.

        static class Node<K,V> implements Map.Entry<K,V> {
            final int hash; //哈希值
            final K key;//key
            V value;//value
            Node<K,V> next; //后置节点
    
            Node(int hash, K key, V value, Node<K,V> next) {
                this.hash = hash;
                this.key = key;
                this.value = value;
                this.next = next;
            }
    
            public final K getKey()        { return key; }
            public final V getValue()      { return value; }
            public final String toString() { return key + "=" + value; }
         //每个节点的hashcode是由key的hashcode和value的hashcode进行异或得到的。
            public final int hashCode() {
                return Objects.hashCode(key) ^ Objects.hashCode(value);
            }
    
            public final V setValue(V newValue) {
                V oldValue = value;
                value = newValue;
                return oldValue;
            }
    
            public final boolean equals(Object o) {
                if (o == this)
                    return true;
                if (o instanceof Map.Entry) {
                    Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                    if (Objects.equals(key, e.getKey()) &&
                        Objects.equals(value, e.getValue()))
                        return true;
                }
                return false;
            }
        }
    

    2.构造方法

    在构造方法中,无论传入的初始容量是多少,HashMap都会通过位运算计算出最接近的2的n次幂的值,从而构造一个2的n次幂的桶

    //默认的初始capacity  
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大capacity
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //默认的load factor
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //存储Node节点的数组
    transient Node<K,V>[] table;
    //HashMap中元素数量的阈值,当元素数量超过阈值时,就会发生扩容
    int threshold;
    
    //负载因子,用于计算HashMap元素数量的阈值,threshold=table.length * loadFactor
    final float loadFactor;
    //初始化一个指定初始化capacity和loadfactor的HashMap
    public HashMap(int initialCapacity, float loadFactor) {
           //边界检查,capacity不能为负数 
        if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
         //如果超过最大的capacity,设置为capacity if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor;
         //threshold由initialCapacity计算得来 this.threshold = tableSizeFor(initialCapacity); }
    /**
     * 根据期望容量cap返回2的n次方的哈希桶的实际容量,返回值一般大于等于cap
     */
    static final int tableSizeFor(int cap) { //假设cap=3
        int n = cap - 1;//n=2,二进制写法是00000010
        n |= n >>> 1;//先右移一位是00000001,或的结果是00000011
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
      //最终n=3,最后返回n+1
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
     
    

      

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
    }
    //初始化一个HashMap,将另一个map的元素放入这个HashMap中。
    public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }
    //将另一个map的所有元素加入表中,参数evict初始化时为false,其他情况为true
    final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        //先获取map的大小
        int s = m.size();
        //如果大于0
        if (s > 0) {
            //如果table此时为null
            if (table == null) { // pre-size
                //根据m的元素数量和负载因子计算出阈值
                float ft = ((float)s / loadFactor) + 1.0F;
            //不能超过最大capacity
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                //如果t大于当前的阈值,返回一个新的满足2的n次方的阈值
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
         //如果此时table不为空且m的元素数量大于threshold
            else if (s > threshold)
                resize();//扩容,以便容纳m的所有元素,有可能出现扩容以后还是无法容纳所有元素,但因为putVal方法里面会扩容。
         //遍历m依次将元素放入HashMap中
            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);
            }
        }
    }
    

    3.hash()方法

    static final int hash(Object key) {
            int h;
            return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
        }

    h>>>16刚好是获取了高位部分,最后是hashcode的低位和高位做异或,增加低位的随机性。

    3.扩容函数  

     扩容函数用于初始化或者扩大哈希桶的大小为原来的两倍,返回扩容后的Node数组。

       如果当前哈希桶为空,分配符合当前阈值的初始容量目标,如果不为空就扩容为原来的两倍。

       

    final Node<K,V>[] resize() {
         //oldTab为当前哈希桶
            Node<K,V>[] oldTab = table;
         //oldCap为当前哈希桶的容量
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            //oldThr为当前的阈值
         int oldThr = threshold;
         //初始化newCap和newThr为0
            int newCap, newThr = 0;
         //如果当前哈希桶容量大于0
            if (oldCap > 0) {
           //如果当前哈希桶容量大于等于最大容量,就只好随你碰撞了
                if (oldCap >= MAXIMUM_CAPACITY) {
              //阈值为int类型的最大值
                    threshold = Integer.MAX_VALUE;
              //返回当前哈希桶,不再扩容
                    return oldTab;
                }
            //newCap等于当前哈希桶容量的两倍,如果这个值小于最大容量,且当前哈希桶容量大于等于默认初始容量
                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;//则新的容量等于当前阈值
            else {               // 如果当前哈希桶为空,且阈值也为0
                newCap = DEFAULT_INITIAL_CAPACITY;//新的容量为默认初始容量
           //新的阈值等于默认负载因子乘以默认初始容量
                newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
            }
            if (newThr == 0) {//如果新的阈值时0,即当前表是空的,但是有阈值,此时newCap等于当前阈值
                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];
         //更新table
            table = newTab;
            if (oldTab != null) {
           //当当前的table数组不为空时,重新分配数组中的数据。
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                //将原链表置空,以便GC
                        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 { // 如果当前节点有后续节点,则要进行重新分配
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                      //留在原位置的节点
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                     //移动到原位置+oldCap的节点
                                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;
        }
    

     扩容后,由于哈希桶的长度变化,集合中的部分元素的位置也会发生变化。JDK8中采用了一个非常巧妙的计算方式,来判断哪些元素需要变动位置,哪些元素不需要。

    下图表示扩容前key1和key2的hash值,以及key1和key2在哈希桶中的位置(为了方便画图只显示8个位,实际上有32个位),此时key1和key2在哈希桶的位置((n-1)&key)是相同的。

    下图是扩容后key1和key2的hash值,此时可以发现,key1的位置并没有变动,而key2的位置发生了变动。

    key2的原位置由5变为21,中间增加了16,而16恰好是原有的哈希桶大小00010000(在resize函数里就是局部变量oldCap)。

    由此发现,位置是否移动,取决于key和新增的一位是1还是0,当key&oldCap等于0(也就是新增的一位为0,如00000101)时,位置不需要移动,当key&oldCap不等于0(新增的一位为1,如00010101),key在哈希桶中的索引为原位置+oldCap。

    JDK8这样优化的好处是,省去了重新计算hash值的时间,由于新增的一位是0还是1是随机的,所以这样一来,就将原有冲突的元素均匀地分到新的位置了。

    4.Put函数

    //put方法实际上调用了putVal方法
    public V put(K key, V value) {
            return putVal(hash(key), key, value, false, true);
    }
     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            //如果table数组为空,则首先扩容,n为扩容后的长度
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            //(n-1)&hash计算当前的key在数组中的位置,如果该位置上还没有结点,直接put
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
           //如果已经有结点了
            else {
                Node<K,V> e; K k;
                //如果结点的key值与要插入的key值相同,直接覆盖
                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);
                //否则就要遍历结点所在链表后,添加在链表的尾部,e此时为头结点
               else {
                    for (int binCount = 0; ; ++binCount) {
                        //当找到链表的尾部时即下一个节点为空,在链表的尾部添加新的结点
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
    //当链表的长度超过8,就调用treeifyBin函数
                   //当tab的大小超过64,就将链表转化为一棵红黑树,否则调用resize进行扩容
    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } //如果在遍历链表的时候发现有结点的key值与要插入的key值相同,退出循环 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //当e不为null时,此时说明链表中存在要插入的key if (e != null) { // existing mapping for key V oldValue = e.value; //当onlyIfAbsent为false,即在存在相同的key值时进行替换(如果为true则不替换) if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); //返回原有的值 return oldValue; } } //modCount加1 ++modCount; if (++size > threshold) //检查是否超过阈值 resize(); afterNodeInsertion(evict); return null; //因为原hashMap中key不存在,所有返回null }

    5.Get函数

        public V get(Object key) {
            Node<K,V> e;
            return (e = getNode(hash(key), key)) == null ? null : e.value;
        }
        /**
         * Implements Map.get and related methods
         *
         * @param hash hash for key
         * @param key the key
         * @return the node, or null if none
         */
        final Node<K,V> getNode(int hash, Object key) {
            Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
            //如果table数组不为空,并且对应位置的链表头结点不为空
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
                //如果key与链表头结点相同,则返回链表头结点
                if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                    return first;
                //如果key与链表头结点不同,则遍历链表
                if ((e = first.next) != null) {
                    if (first instanceof TreeNode)
                        return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                    do {
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            return e;
                    } while ((e = e.next) != null);
                }
            }
            return null;
        }

    6.Remove方法

     /**
         * Removes the mapping for the specified key from this map if present.
         *
         * @param  key key whose mapping is to be removed from the map
         * @return the previous value associated with <tt>key</tt>, or
         *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
         *         (A <tt>null</tt> return can also indicate that the map
         *         previously associated <tt>null</tt> with <tt>key</tt>.)
         */
        public V remove(Object key) {
            Node<K,V> e;
            return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
        }
    
        /**
         * Implements Map.remove and related methods
         *
         * @param hash hash for key
         * @param key the key
         * @param value the value to match if matchValue, else ignored
         * @param matchValue if true only remove if value is equal
         * @param movable if false do not move other nodes while removing
         * @return the node, or null if none
         */
        final Node<K,V> removeNode(int hash, Object key, Object value,
                                   boolean matchValue, boolean movable) {
            Node<K,V>[] tab; Node<K,V> p; int n, index;
         //如果table数组不为空,并且对应位置链表的头结点不为空 if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) { Node<K,V> node = null, e; K k; V v;
    //如果要删除的key与链表头结点的key相同,则说明要删除的结点就是链表头结点 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p;
    //遍历以找到要删除的结点,以及要删除结点的上一个结点p else if ((e = p.next) != null) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break; } p = e; } while ((e = e.next) != null); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); else if (node == p)//如果要删除的结点是链表的头结点 tab[index] = node.next;//将table数组对应位置赋值给其下一个结点 else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null; }

     

    JDK7中的HashMap

    一.链表节点Entry

    jdk 1.7中用Entry作为链表节点类。

    static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            int hash;
    
            /**
             * Creates new entry.
             */
            Entry(int h, K k, V v, Entry<K,V> n) {
                value = v;
                next = n;
                key = k;
                hash = h;
            }
    
            public final K getKey() {
                return key;
            }
    
            public final V getValue() {
                return value;
            }
    
            public final V setValue(V newValue) {
                V oldValue = value;
                value = newValue;
                return oldValue;
            }
    
            public final boolean equals(Object o) {
                if (!(o instanceof Map.Entry))
                    return false;
                Map.Entry e = (Map.Entry)o;
                Object k1 = getKey();
                Object k2 = e.getKey();
                if (k1 == k2 || (k1 != null && k1.equals(k2))) {
                    Object v1 = getValue();
                    Object v2 = e.getValue();
                    if (v1 == v2 || (v1 != null && v1.equals(v2)))
                        return true;
                }
                return false;
            }
    
            public final int hashCode() {
                return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
            }
    
            public final String toString() {
                return getKey() + "=" + getValue();
            }
    
            /**
             * This method is invoked whenever the value in an entry is
             * overwritten by an invocation of put(k,v) for a key k that's already
             * in the HashMap.
             */
            void recordAccess(HashMap<K,V> m) {
            }
    
            /**
             * This method is invoked whenever the entry is
             * removed from the table.
             */
            void recordRemoval(HashMap<K,V> m) {
            }
        }

    Entry是HashMap的内部类, 从成员变量可以看出

    • key是HashMap中的key
    • value是HashMap中的value
    • next指向下一个链表节点
    • hash表示hash值

    二.Put方法

     public V put(K key, V value) {
            if (table == EMPTY_TABLE) {
                inflateTable(threshold);
            }
            if (key == null)
                return putForNullKey(value);
            int hash = hash(key);
            int i = indexFor(hash, table.length);
            for (Entry<K,V> e = table[i]; e != null; e = e.next) {
                Object k;
                if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                    V oldValue = e.value;
                    e.value = value;
                    e.recordAccess(this);
                    return oldValue;
                }
            }
    
            modCount++;
            addEntry(hash, key, value, i);
            return null;
        }

    第一步,判断数组是否为空,是否需要初始化

    第二步,如果key为null,则put一个空值进去

    第三步,计算key的哈希值

    第四步,根据哈希值定位到对应的桶,即在数组中的位置

    第五步,遍历桶中的链表,看是否找到hashcode相等的、key相等的节点,如果有则覆盖原来的值,返回原来的值

    第六步,如果找不到hashcode相等和key相等的节点,就将其添加到桶对应的链表的头部(从addEntry方法可以看出)

    1)hash方法和indexFor方法

    indexFor方法输入key的hash值和桶的大小,返回桶的位置。

     /**
         * Returns index for hash code h.
         */
        static int indexFor(int h, int length) {
            // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
            return h & (length-1);
        }
    

    indexFor方法返回桶的位置,因为length总是等于2的n次方,所以h&(length-1)时相当于对length取模(即h%length)。

    因为HashMap中桶的大小为2的n次幂,所以与运算的结果是高位全部归零,低位作为index.例如此时桶大小为16,16-1=15,二进制表示0000000000000000000001111

         1010010111000100001000101
    &    0000000000000000000001111
    ---------------------------------------
         0000000000000000000000101

    得到的结果是截取了最后四位,如果只通过最后四位来决定散列的结果,容易导致碰撞,所以需要用hash函数来进行“扰动”。

    所以HashMap中元素的哈希值不仅仅是key的hashcode而已,而是经过了一些位运算,增加低位的随机性,从而减少碰撞。

    /**
         * Retrieve object hash code and applies a supplemental hash function to the
         * result hash, which defends against poor quality hash functions.  This is
         * critical because HashMap uses power-of-two length hash tables, that
         * otherwise encounter collisions for hashCodes that do not differ
         * in lower bits. Note: Null keys always map to hash 0, thus index 0.
         */
        final int hash(Object k) {
            int h = hashSeed;
            if (0 != h && k instanceof String) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
    
            h ^= k.hashCode();
    
            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
        }

    2)addEntry和createEntry方法

      /**
         * Adds a new entry with the specified key, value and hash code to
         * the specified bucket.  It is the responsibility of this
         * method to resize the table if appropriate.
         *
         * Subclass overrides this to alter the behavior of put method.
         */
        void addEntry(int hash, K key, V value, int bucketIndex) {
            if ((size >= threshold) && (null != table[bucketIndex])) {
                resize(2 * table.length);
                hash = (null != key) ? hash(key) : 0;
                bucketIndex = indexFor(hash, table.length);
            }
    
            createEntry(hash, key, value, bucketIndex);
        }
        
         void createEntry(int hash, K key, V value, int bucketIndex) {
    //获取了桶中链表的头结点 Entry<K,V> e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++; }

     HashMap通过调用addEntry方法来添加一个Entry对象,hash表示hashcode,key/value不用说,bucketIndex是桶的位置即数组的索引。

     1.如果容器大小超过了阈值并且桶不为空,则要扩容成原来的两倍,并且将当前的key重新hash重新定位得到新的bucketIndex'

        2.最终都调用了createEntry方法,createEntry首先获取了桶中链表的头结点(有可能是null)

        3.创建了一个新的Entry对象,并将Entry中的next指向原来的头结点,也就是说新的Entry成为了桶中链表的新的头结点。

    三.扩容函数resize方法

    void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
        }
    
        /**
         * Transfers all entries from current table to newTable.
         */
        void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }

    1.首先判断现在的容量是否等于最大容量,如果大于那么阈值就等于int中的最大值(意味着已经达到最大容量了,不能再扩容了)

    2.如果现在的容量小于最大容量,那么就创建一个新的Entry数组,将旧的Entry对象转移到新的Entry数组中。

    3.将阈值更新为新的容量乘以负载因子和最大容量中的较小值。

    四.get方法

    public V get(Object key) {
            if (key == null)
                return getForNullKey();
            Entry<K,V> entry = getEntry(key);
    
            return null == entry ? null : entry.getValue();
        }
        /**
         * Returns the entry associated with the specified key in the
         * HashMap.  Returns null if the HashMap contains no mapping
         * for the key.
         */
        final Entry<K,V> getEntry(Object key) {
            if (size == 0) {
                return null;
            }
    
            int hash = (key == null) ? 0 : hash(key);
            for (Entry<K,V> e = table[indexFor(hash, table.length)];
                 e != null;
                 e = e.next) {
                Object k;
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }
            return null;
        }

    1.如果key为null,就获取key值为null的value

    2.如果key非空,如果桶的大小为0,返回null.否则计算key的hash值,定位所在桶的位置

    3.遍历桶中的元素,当有hashcode相同,key相同时,则返回对应的值,如果找不到则返回null

    三、jdk8的HashMap的优化

    1.在jdk7的HashMap中,当Hash冲突严重时,桶上的链表就会越来越长,从而导致查询效率降为O(N)

      jdk8的HashMap在桶上的链表达到8时,会将链表转化为红黑树。

    2.jdk7中,数组扩容后,通过key的hashcode对数组长度进行取模的方式来调整数组中的元素

    jdk8中,数组扩容后,通过key的hashcode与原来的size进行与运算(因为扩容后是原来size的两倍),若等于0,则不需要移动,否则就移动原来的索引加上size的大小。

    由于与运算的结果可以说是随机的,所以jdk8的解决办法使得元素分布得更加均匀,并且不会倒置。

    四、HashMap中的线程安全问题

    1.jdk7的线程安全问题-->环形链表

    为什么在多线程的情况下,向HashMap放入元素会导致死循环呢?

    主要是在扩容后转移链表节点时导致形成了环形链表

    以jdk7中的resize为例

    void resize(int newCapacity) {
            Entry[] oldTable = table;
            int oldCapacity = oldTable.length;
            if (oldCapacity == MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return;
            }
    
            Entry[] newTable = new Entry[newCapacity];
            transfer(newTable, initHashSeedAsNeeded(newCapacity));
            table = newTable;
            threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
        }
    
        /**
         * Transfers all entries from current table to newTable.
         */
        void transfer(Entry[] newTable, boolean rehash) {
            int newCapacity = newTable.length;
            for (Entry<K,V> e : table) {
                while(null != e) {
                    Entry<K,V> next = e.next;
                    if (rehash) {
                        e.hash = null == e.key ? 0 : hash(e.key);
                    }
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                }
            }
        }
    

     例如以下这个场景,线程一执行到Entry<K,V> next=e.next这一句时,e指向KEY=3的节点,next指向KEY=7的节点

    此时CPU时间片轮转,执行权交给了线程二,线程二执行了所有的代码。

     

    执行权又回到了线程一,线程一调用indexFor方法获取到KEY=3的最新位置,并将KEY=3的next指向了KEY=7。

    此时KEY=3和KEY=7形成了环形链表,e就一直不等于NULL,所以一直无法跳出循环。

    而在jdk8中,链表的插入由头插法变成了尾插法,所以就算在resize过程中有多个线程执行代码,因为元素在链表中的顺序与之前相同,也就是说next还是在e的后面,就不会出现环形链表的情况。

    2.jdk8的线程安全问题,多线程下相互覆盖

    if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
                  tab[i] = newNode(hash, key, value, null);
    

    put方法中如果正好线程A,B所持有的不同key对应的hash值相同,就可能导致数据丢失,例如线程A发现没有hash碰撞,正准备插入元素时,发生CPU时间片轮换,线程B也同时发现没有hash碰撞,直接插入元素,而线程被唤醒后,直接将值写入,导致线程B写入的值丢失

     参考链接

    https://tech.meituan.com/java_hashmap.html

    https://crossoverjie.top/2018/07/23/java-senior/ConcurrentHashMap/

    https://www.zhihu.com/question/20733617 HashMap中的hash方法解析

    https://www.cnblogs.com/dongguacai/p/5599100.html HashMap中的线程安全问题

    https://mp.weixin.qq.com/s/VtIpj-uuxFj5Bf6TmTJMTw

  • 相关阅读:
    CSS之旅——第二站 如何更深入的理解各种选择器
    CSS之旅——第一站 为什么要用CSS
    记录一些在用wcf的过程中走过的泥巴路 【第一篇】
    asp.net mvc 之旅—— 第二站 窥探Controller下的各种Result
    asp.net mvc 之旅—— 第一站 从简单的razor入手
    Sql Server之旅——终点站 nolock引发的三级事件的一些思考
    Sql Server之旅——第十四站 深入的探讨锁机制
    Sql Server之旅——第十三站 对锁的初步认识
    Sql Server之旅——第十二站 sqltext的参数化处理
    Sql Server之旅——第十一站 简单说说sqlserver的执行计划
  • 原文地址:https://www.cnblogs.com/qingfei1994/p/9151915.html
Copyright © 2011-2022 走看看