java-HahMap相关问题

概述

文章对HashMap的部分细节进行介绍,JDK1.7之前有可能出现环形表的问题,而1.7之后进行了改进,文章对环形表现象的出现进行了解析,然后对HashMap注意的几个问题进行了解答。
HashMap的底层实现是数组,主要具有以下特点 :

  • 键值对都允许为空(重要)
  • 线程不安全
  • 不保证有序

jdk1.7 迁移过程

首先看一下数据迁移的地方在哪里?JDK1.7 HashMap

public V put(K key, V value)
{
    ......
    //算Hash值
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    //如果该key已被插入,则替换掉旧的value (链接操作)
    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++;
    //该key不存在,需要增加一个结点
    addEntry(hash, key, value, i);
    return null;
}



//增加一节点
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);
    //查看当前的size是否超过了我们设定的阈值threshold,如果超过,需要resize
    if (size++ >= threshold)
        resize(2 * table.length);
}


//重新生成空间
void resize(int newCapacity)
{
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    ......
    //创建一个新的Hash Table
    Entry[] newTable = new Entry[newCapacity];
    //将Old Hash Table上的数据迁移到New Hash Table上
    transfer(newTable);
    table = newTable;
    threshold = (int)(newCapacity * loadFactor);
}


//数据迁移过程
void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    //下面这段代码的意思是:
    //  从OldTable里摘一个元素出来,然后放到NewTable中
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;  //NO.1
                int i = indexFor(e.hash, newCapacity);  //NO.2
                e.next = newTable[i]; //NO.3 把当前位置的节点放在新插进来节点的next(于是这里的当前位置在下面一步就会变成久节点了)
                newTable[i] = e;  //NO.4 当前位置的节点放新插入的节点
                e = next; //NO.5  e 换成链表中的下一位
            } while (e != null);
        }
    }

}

正常的情况下,下图可以看见久的往后移,新的往前插(饭堂排队吃饭插队一样)

1297993-20191230175018110-716752968.jpg

并发情况下,假如有两个线程执行,

  • 线程1执行到 NO.1时
  • 线程2执行完了
    就会出现如下

1297993-20191230180641217-1007710848.jpg

jdk1.8 迁移过程

    /* ---------------- Fields -------------- */

    /**
     * The table, initialized on first use, and resized as
     * necessary. When allocated, length is always a power of two.
     * (We also tolerate length zero in some operations to allow
     * bootstrapping mechanics that are currently not needed.)
     * 
     *  存放数据的结构 ,常以两倍大小扩容
     * 
     */
    transient Node<K,V>[] table;

    /**
     * Holds cached entrySet(). Note that AbstractMap fields are used
     * for keySet() and values().
     * 
     *  方便遍历用的
     * 
     */
    transient Set<Map.Entry<K,V>> entrySet;

    /**
     * The number of key-value mappings contained in this map.
     * 
     *  数量
     * 
     */
    transient int size;

    /**
     * 
     * 在 JDK 1.8 中,HashMap 中的 modCount 字段是用于记录 HashMap 结构修改次数的。具体来说,modCount 字段记录了 HashMap 中插入、删除、扩容等操作的次数,每进行一次这样的操作,modCount 字段的值就会自增 1。这个字段主要用于实现 HashMap 的迭代器(Iterator)和 Spliterator,以便在迭代过程中检测 HashMap 是否被修改。

     * 在 JDK 1.8 中,HashMap 的迭代器(Iterator)和 Spliterator 都是快速失败(fail-fast)的,即在迭代过程中如果发现 HashMap 被修改了,就会抛出 ConcurrentModificationException 异常,以保证迭代器的正确性。为了实现快速失败机制,HashMap 在迭代器和 Spliterator 中都需要检测 modCount 字段是否与迭代器或 Spliterator 创建时的值相等,如果不相等就抛出 ConcurrentModificationException 异常。

     * 需要注意的是,modCount 字段并不是线程安全的,如果多个线程同时对 HashMap 进行修改,可能会导致 modCount 字段的值不一致。因此,在多线程环境下使用 HashMap 时,应该使用线程安全的 ConcurrentHashMap 类,以避免这种问题。
     * 
     */
    transient int modCount;

    /**
     * The next size value at which to resize (capacity * load factor).
     *
     *  下次扩容的大小
     * 
     * 
     * @serial
     */
    int threshold;

    /**
     * The load factor for the hash table.
     * 
     *  负载因子 
     *
     * @serial
     */
    final float loadFactor;
    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;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            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;
    }    


    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        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;
            }
            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 {               // zero initial threshold signifies using defaults
            // 没有初始化大小 ,第一次
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

        // 把新创建的 tab 赋值给原来的那个 table 
        table = newTab;
        if (oldTab != null) {

            // 这是扩容后 , 迁移原有 tab 上的元素
            // 变量原有数组上的元素
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        // 1. next 指针为空表示那个槽位只有一个元素 , 直接放在新的位置(重新计算位置)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        // 2. 假如是红黑树 ,那么插入到红黑树那里去 
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        // 3. 都不是, 那肯定就是链表上的 node 

                        // 链表的迁移分两部分, 一部分是 hash 值和旧空间大小与 == 0 的 , 不移动位置 ; 其余的移动位置
                        // 下面是移动位置
                        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;
                            }
                            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;
    }

resize 操作有点难懂 ,用下面这张图就明白了
img

img

问题

图片来源与参考资料。

哈希表如何解决Hash冲突

1297993-20191231111704731-1516410864.png

JDK1.7之后换成了尾插法,不会出现环形表的但是依然是线程不安全的。

HashMap 中的 key若 Object类型, 则需实现哪些方法?

1297993-20191231111746591-1341576390.png

HashMap 扩容时机及扩容时避免rehash的优化

每次增加节点的时候 ,都会检查一下容量 ,当达到一个阈值的时候就会触发扩容机制 ,扩容时避免rehash的优化可以增加一下负载因子 , 提高空间利用率 .

为什么使用红黑树

  1. 使用红黑树可以降低树高
  2. 红黑树增删改查平均时间复杂度为log(n) , 且时间复杂度最差情况都是log(n)
    那为什么不使用平衡二叉树
    主要原因是红黑树的平衡性能更好。红黑树是一种自平衡的二叉查找树,它能够在O(log n)的时间复杂度内完成插入、删除和查找等操作。相比之下,平衡二叉树需要对每个节点计算平衡因子,以判断是否需要进行旋转等操作,因此在实现和维护上相对复杂,性能也相对较低。(红黑树旋转次数比平衡二叉树少)

HashMap,HashTable,ConcurrentHashMap的共同点和区别?

相同点 : 内部底层数据结构都是数组 , 都会扩展 ,都是利用hash 确定位置
区别 : HashMap 不是线程安全的;HashTable 和 ConcurrentHashMap则是线程安全的; HashMap允许 null 值 和 null 键 而 HashTable 不允许 ;

hashmap 为什么是线程不安全的

头插法指针在指向的时候有可能形成回环, Java8以后改进成尾插法 ,但是依旧不是线程安全的

参考资料

这是之前写过的博客

posted @ 2019-12-31 14:31  float123  阅读(156)  评论(0编辑  收藏  举报