美文网首页
HashMap(jdk1.6)梳理

HashMap(jdk1.6)梳理

作者: lsr_flying | 来源:发表于2020-06-27 23:14 被阅读0次

    HashMap是我们平时开发中接触得比较多的数据容器之一。jdk1.8之后还对HashMap进行了优化。本篇将首先介绍jdk1.6版本的HashMap相关知识,涉及到jdk1.8的后续章节补充。我们将围绕如下几点展开:HashMap数据结构、存储和resize过程、fail-fast机制。

    1. HashMap的基本数据结构

    HashMap是K-V型容器,对于每一个K-V值,保存在内部HashMap.Entry对象中。对于不同的Entry对象,依据哈希值,保存在数组字段table中。针对Hash冲突的情况,采用链式地址法,即Entry对象可以形成一个链表结构,指向相同哈希的下一个值。

    public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable
    {
         /**
         * The table, resized as necessary. Length MUST Always be a power of two.
         */
        transient Entry[] table;
        //......
        static class Entry<K,V> implements Map.Entry<K,V> {
            final K key;
            V value;
            Entry<K,V> next;
            final int hash;
        }
    }
    

    2. put的基本流程

    2.1 大致流程:

    (0). 根据key的hash值,获取对应的位置索引;
    (1). 判断k-v值是否已存在,存在则更新,结束;
    (2). k-v值不存在,插入节点,采用头插法(插入到已有链表元素的前面);
    (3). 判断当前是否需要扩容,否,结束;
    (4). 如果需要扩容,创建新表,把旧表数据依次按照头插法,插入对应的bucket及链表。
    注意: 如果是putAll,则会事先判断是否需要resize,然后再循环执行put方法。

    2.2 步骤详解

    public V put(K key, V value) {
            if (key == null)
                return putForNullKey(value);
            // 对应步骤2.1-(0),根据key哈希,获取索引位置。注意之所以要再hash一次,是为了增加扰动,提高哈希值的随机性和均匀分布性
            int hash = hash(key.hashCode());
            int i = indexFor(hash, table.length);
    
            //对应步骤2.1-(1),判断对应的k值是否存在,存在则更新
            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++;
            //对应步骤2.1-(2)(3)(4),插入节点,并判断新添加元素后,是否需要扩容
            addEntry(hash, key, value, i);
            return null;
        }
    
    void addEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
            // 获取当前索引的头部元素,并且把新值的next指针指向头部元素,然后新插入的元素就做为新的头部元素-头插入法
            table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
            // 判断是否需要扩容,后续介绍
            if (size++ >= threshold)
                resize(2 * table.length);
        }
    

    3. resize过程

    resize的基本思想还是比较简单的。就是把旧的table[]中的元素,一个一个地根据原先的哈希规则,保存到新的扩容好的table[]对象中。然后使用新的table[]对象即可。当然,这些都是基于单线程的,在多线程的条件下,会出问题,此处先不分析。我们直接从代码入手,看下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);
            // 指向新数组
            table = newTable;
            threshold = (int)(newCapacity * loadFactor);
        }
    
        /**
         * Transfers all entries from current table to newTable.
         */
        void transfer(Entry[] newTable) {
            Entry[] src = table;
            int newCapacity = newTable.length;
            // 外层for循环,按顺序读取每一个bucket的Entry对象
            for (int j = 0; j < src.length; j++) {
                Entry<K,V> e = src[j];
                if (e != null) {
                    // 旧数组的bucket指针置null
                    src[j] = null;
                    // 遍历当前bucket的所有链表值,并根据最新的容量,计算新的索引值,然后采用头插法,插入新值
                    do {
                        Entry<K,V> next = e.next;
                        int i = indexFor(e.hash, newCapacity);
                        e.next = newTable[i];
                        newTable[i] = e;
                        e = next;
                    } while (e != null);
                }
            }
        }
    

    4. fail-fast机制

    fail-fast机制,就是指在使用迭代器遍历元素的过程当中,如果在其它线程中对HashMap进行元素增加或删除时,迭代器会抛ConcurrentModificationException。需要注意的是,fail-fast机制是一种尽最大努力抛异常的机制,业务开发中,不能依赖这种机制。
    fail-fast机制的实现,直观上理解还是比较简单的,其主要依赖如下俩字段,modCount和expectedModCount,并且判断两者是否相等:

    public class HashMap<K,V>
        extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable
    {
        transient volatile int modCount;
        private abstract class HashIterator<E> implements Iterator<E> {
            // ......
            int expectedModCount;   // For fast-fail
           
            HashIterator() {
                // 在新建一个迭代器的时候,会保存新建时的修改次数
                expectedModCount = modCount;
                // ......
            }
    
            final Entry<K,V> nextEntry() {
                // 因为modeCount是跟随HashMap的,所以如果有别的线程对HashMap进行了元素的增删(注意,不包括修改),那么modeCount就会计数加一;而expectedModCount是初始化时就定下来的,那么通过比较修改次数,就可以发现HashMap被修改了。
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                // ......
            }
    }
    

    6. 总结

    6.1 需关注的点

    (1) capacity一定是2的n次方。在初始化的时候指定了capacity大小,则会挑选一个比指定capacity大的最小的2的n次方的数。

    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;
    

    6.2 可能产生的问题

    (1) 并发put后,在get时可能产生死循环,消耗CPU:
    这个主要是因为resize的原因,resize完后,新的链表相对于原来的链表,是倒置过的,即,原来某个bucket的链表是:bucket[i]=A->B->C,因为HashMap扩容新元素采用的是头插法,这里我们假设A\B\C三个元素,扩容后还是属于同一个bucket。那么新的链表中就会变成:newBucket[i]=C->B->A。
    接下来,我们加入并发操作的情况。假设另一个线程也引起了resize操作,线程1顺利地修改了链表,线程2之前已经读取了A元素,现在准备插入A元素到newBucket[i],此时的newBucket[i]=C->B->A,因为线程2的操作,又变成了newBucket[i]=A->C->B->A,出现是循环了。
    (2) 并发put时,可能导致元素丢失:
    我们知道,要插入新元素,会调addEntry方法,该方法会在某个bucket的头部插入新值,具体是插入新值后,再修改bucket的指向指针,如果有两个线程都同时执行完了new Entry()方法,那么就会出现两次table[bucketIndex]的赋值,显然有一次的插入就会丢失了。

    void addEntry(int hash, K key, V value, int bucketIndex) {
            Entry<K,V> e = table[bucketIndex];
            table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
            // ......
        }
    

    (3)put非null元素后get出来的却是null:
    transfer方法中,会先把bucket置null,这时候如果刚好有线程去读这个bucktet,那拿出来的就是null了。

     void transfer(Entry[] newTable) {
            // ......
            // 外层for循环,按顺序读取每一个bucket的Entry对象
            for (int j = 0; j < src.length; j++) {
                Entry<K,V> e = src[j];
                if (e != null) {
                    // 旧数组的bucket指针置null
                    src[j] = null;
                   // ......
                }
            }
        }
    

    相关文章

      网友评论

          本文标题:HashMap(jdk1.6)梳理

          本文链接:https://www.haomeiwen.com/subject/zweuxktx.html