JDK7中HashMap存在的问题分析,你知道哪些?

简介: JDK7中HashMap存在的问题分析,你知道哪些?

一、JDK7中HashMap源码中重要的参数

二、JDK7中HashMap的构造方法

三、JDK7中创建一个HashMap的步骤

四、JDK7中HashMap的put方法执行流程的分析

(一)图解JDK7中HashMap的put方法流程

(二)JDK7中HashMap源码分析put方法执行流程

五、JDK7中HashMap的get方法执行流程的分析

六、JDK7中HashMap存在的问题

(一)多线程情况下扩容造成的循环链表问题

有对比才有伤害,都说多线程情况下HashMap会造成循环链表,究竟是如何造成的呢?

我自己在学习的时候,把单线程和多线程情况下分别拿出来对比一下这个过程是如何进程的,这样就可以方便理解了。

我们假设现在的存储结构是这样的:

1、单线程扩容详解(图解+源代码结合分析)

在JDK7中,HashMap的扩容功能在put方法中,执行流程如下:

这里我画了一个图,是从put(...)方法---(到)--->执行扩容机制的流程图。

注:这是根据我自己的理解和相关源代码得出来的一个图,难免会有不足或者错误的地方,如果有的话,欢迎指正交流

resize方法的代码如下:

/**
     * 将此映射的内容重新映射到容量更大的新数组中。
     * 当此映射中的键数达到其阈值时,将自动调用此方法。
     *
     * 如果当前容量为MAXIMUM_CAPACITY,则此方法不会调整Map的大小,
     * 而是将阈值设置为Integer.MAX_VALUE。
     * 这可以防止将来调用。
     *
     *
     *        newCapacity新容量,必须是2的幂;
     *           除非当前容量为MAXIMUM_CAPACITY(在这种情况下该值无关紧要),
     *           否则该值必须大于当前容量。
     */
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }
    //根据源来的table容量生成一个2倍的容量的table
    Entry[] newTable = new Entry[newCapacity];
    // 新的数组的容量
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    //将扩容后新的table赋值给全局的table,从而完成了替换
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

在resize()方法中需要调用transfer()方法进行数据转移:下面就是最核心的扩容代码了!

/**
     * 将所有Entry从当前表转移到newTable。
     */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;//容量
    for (Entry<K,V> e : table) { //遍历table[1,2,3,4,5]
        while(null != e) { //遍历table中的链表table[i]
            Entry<K,V> next = e.next;
            if (rehash) {//如果是重新Hash,则需要重新计算hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); //定位Hash桶
            //元素连接到桶中,这里相当于单链表的插入,总是插入在最前面,指针指向他下面的一个元素
            e.next = newTable[i];
            //newTable[i]的值总是最新插入的值
            newTable[i] = e;
            //继续下一个元素
            e = next;
        }
    }
}

扩容前与扩容后的容量:

对于扩容后的数据迁移操作:

也就是下面这个代码:

/**
     * 将所有Entry从当前表转移到newTable。
     */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;//容量
    for (Entry<K,V> e : table) { //遍历table[1,2,3,4,5]
        while(null != e) { //遍历table中的链表table[i]
            Entry<K,V> next = e.next;
            if (rehash) {//如果是重新Hash,则需要重新计算hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); //定位Hash桶
            //元素连接到桶中,这里相当于单链表的插入,总是插入在最前面,指针指向他下面的一个元素
            e.next = newTable[i];
            //newTable[i]的值总是最新插入的值
            newTable[i] = e;
            //继续下一个元素
            e = next;
        }
    }
}

先来简单的分析一下这段代码,这段代码的结构是这样子的:

for (Entry<K,V> e : table) {  //遍历每个桶
    while(null != e) { //遍历每个桶的链表
        ...
    }
}

最核心的就是while中的代码了:

Entry<K,V> next = e.next;
if (rehash) {//如果是重新Hash,则需要重新计算hash值
    e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); //定位Hash桶
//元素连接到桶中,这里相当于单链表的插入,总是插入在最前面,指针指向他下面的一个元素
e.next = newTable[i];
//newTable[i]的值总是最新插入的值
newTable[i] = e;
//继续下一个元素
e = next;

我们假设rehash一直等于false,那么还剩下:下面这些代码就是迁移数据的主要流程

Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity); //定位Hash桶
//元素连接到桶中,这里相当于单链表的插入,总是插入在最前面,指针指向他下面的一个元素
e.next = newTable[i];
//newTable[i]的值总是最新插入的值
newTable[i] = e;
//继续下一个元素
e = next;

先来看:

Entry<K,V> next = e.next;

如果看不懂这个地方的话,你可能需要去重新回顾一下链表的知识了。

此时e指向的是是第1个Entry(我们通常就叫节点,下面我就以“节点称呼”);

而e.next指向的就是第1个节点的next节点。

如下图所示:

然后重新计算在扩容后的table中的位置:

这个位置是有讲究的。

int i = indexFor(e.hash, newCapacity); //定位Hash桶

这个重新计算之后的i,不是在原先的位置,就是在扩容之后的另一半的位置:入如下图红色的节点

我们假设都在原先的位置处

下面:

//元素连接到桶中,这里相当于单链表的插入,总是插入在最前面,指针指向他下面的一个元素
e.next = newTable[i];

//newTable[i]的值总是最新插入的值
newTable[i] = e;

继续执行:

//继续下一个元素
e = next;

随后又开始新一轮的while循环:

Entry<K,V> next = e.next;

然后:

e.next = newTable[i];

然后:

newTable[i] = e;

然后:

//继续下一个元素
e = next;

然后又是新一轮的while循环:

最后指向null的时候,while循环结束

while循环结束,此链表也迁移成功:

发现了什么?

成逆向的了

到此为止,扩容结束

//将扩容后新的table赋值给全局的table,从而完成了替换
table = newTable;

2、多线程扩容讲解(图解+源代码结合分析)

都说多线程情况下HashMap是不安全的,会形成循环链表,甚至会造成计算机宕机。

我们来亲自做一下实验。

这里我们假设有两个线程线程A线程B

线程A和线程B同时都在做同一件事情:扩容

我们本次演示的就是两个线程同时扩容的过程。由于有了单线程扩容的基础知识,多线程这里就不再过多的解释代码了,关心的比较多的是多线程操作的过程。

两个线程同时调用了put方法,同时进入addEntry方法:

同时进入resize方法:

同时new了一个newTable(扩容两倍的table):

同时进行扩容:

最终两个线程都执行到了下面这个地方中:并且同时执行

/**
     * Transfers all entries from current table to newTable.
     * 将所有Entry从当前表转移到newTable。
     */
void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;//容量
    for (Entry<K,V> e : table) { //遍历table[1,2,3,4,5]
        while(null != e) { //遍历table中的链表table[i]
            Entry<K,V> next = e.next;
            if (rehash) {//如果是重新Hash,则需要重新计算hash值
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            int i = indexFor(e.hash, newCapacity); //定位Hash桶
            //元素连接到桶中,这里相当于单链表的插入,总是插入在最前面,指针指向他下面的一个元素
            e.next = newTable[i];
            //newTable[i]的值总是最新插入的值
            newTable[i] = e;
            //继续下一个元素
            e = next;
        }
    }
}

现在的状况:

1、线程A和线程B都有各自扩容后的newTable表

2、线程A和线程B现在公用公共的原table数据

进入到while循环后的状态:

  • eA和nextA分别代表的是线程A的e和e.next;
  • eA和nextB分别代表的是线程B的e和e.next。

我们假设线程A执行完毕:

下图的结果想必大家应该不否认吧。

链表的特性就是不管你这个节点与谁连接,之前连接着的是不会断开的

如果下图看不懂的话,请不要继续往下看了,务必要先把此图看明白。

现在我们只需要讨论下图中的线程A和线程B即可:

由于线程A先执行完毕,现在线程B要在线程A执行后的基础上执行。

//newTable[i]的值总是最新插入的值
newTable[i] = e;

//继续下一个元素
e = next;

现在第一次while循环结束,第二次循环开始:

Entry<K,V> next = e.next;

e.next = newTable[i];

因为e.next本来就是指向e.next的所以这里不变化。往后会越来越奇怪!!!!!!

//newTable[i]的值总是最新插入的值
newTable[i] = e;

//继续下一个元素
e = next;

继续while循环:

Entry<K,V> next = e.next;

这一步就出现问题了,在这次循环中,如果正确的话,应该是指向TrueDei-3这个节点的。

继续执行:

e.next = newTable[i];

现在发现了什么??????

居然形成一个循环了,这样CPU就是不停的走呀走呀走呀走。。。。最终把CPU

居然形成一个循环了,这样CPU就是不停的走呀走呀走呀走。。。。最终把CPU的资源给耗尽。

这就是JDK7中的HashMap在多线程情况下出现的问题。


目录
相关文章
|
8月前
|
Java
【JDK 源码分析】HashMap 操作方法
【1月更文挑战第27天】【JDK 源码分析】HashMap Put 元素 初始化
|
3月前
|
Java
Java基础之 JDK8 HashMap 源码分析(中间写出与JDK7的区别)
这篇文章详细分析了Java中HashMap的源码,包括JDK8与JDK7的区别、构造函数、put和get方法的实现,以及位运算法的应用,并讨论了JDK8中的优化,如链表转红黑树的阈值和扩容机制。
49 1
|
5月前
|
存储 Java
【Java集合类面试七】、 JDK7和JDK8中的HashMap有什么区别?
JDK7中的HashMap使用数组加链表解决冲突,而JDK8增加了红黑树结构以优化链表过长时的性能,提高查找效率。
|
7月前
|
存储 算法 安全
JDK源码分析-HashMap
JDK源码分析-HashMap
|
设计模式 算法 Java
面试官:JDK1.8 HashMap扩容rehash算法是如何优化的?
本文跟大家聊一聊一个常见的面试题,那就是JDK1.8 HashMap扩容rehash算法是如何优化的?
|
8月前
|
安全 Java
【JDK 源码分析】HashMap 线程安全问题分析
【1月更文挑战第27天】【JDK 源码分析】HashMap 线程安全问题分析
|
8月前
|
存储 Java
【JDK 源码分析】HashMap 底层结构
【1月更文挑战第27天】【JDK 源码分析】HashMap 底层结构
|
8月前
|
存储 安全 Java
Hashtable和HashMap:差异,数据结构概述,以及JDK的影响
Hashtable和HashMap:差异,数据结构概述,以及JDK的影响
58 0
|
3月前
|
Java
让星星⭐月亮告诉你,HashMap中保证红黑树根节点一定是对应链表头节点moveRootToFront()方法源码解读
当红黑树的根节点不是其对应链表的头节点时,通过调整指针的方式将其移动至链表头部。具体步骤包括:从链表中移除根节点,更新根节点及其前后节点的指针,确保根节点成为新的头节点,并保持链表结构的完整性。此过程在Java的`HashMap$TreeNode.moveRootToFront()`方法中实现,确保了高效的数据访问与管理。
37 2
|
3月前
|
Java 索引
让星星⭐月亮告诉你,HashMap之往红黑树添加元素-putTreeVal方法源码解读
本文详细解析了Java `HashMap` 中 `putTreeVal` 方法的源码,该方法用于在红黑树中添加元素。当数组索引位置已存在红黑树类型的元素时,会调用此方法。具体步骤包括:从根节点开始遍历红黑树,找到合适位置插入新元素,调整节点指针,保持红黑树平衡,并确保根节点是链表头节点。通过源码解析,帮助读者深入理解 `HashMap` 的内部实现机制。
50 2