Java进阶:HashMap底层原理(通俗易懂篇)

java,hashmap · 浏览次数 : 61

小编点评

在Java 7及之前版本的HashMap中,底层数据结构主要由数组和链表组成。数组是一个Entry数组,其中每个元素都是一个Entry节点,这些节点包含键值对以及指向下一个Entry的引用。当多个键映射到同一索引时,它们会形成链表以解决哈希冲突。 从Java 8开始,HashMap引入了红黑树作为底层数据结构的优化手段,以提高在大量哈希冲突情况下的查找性能。红黑树是一种自平衡二叉查找树,其查找、插入和删除操作的时间复杂度均为O(log n)。 在Java 7中,HashMap的扩容采用头插法,即在链表头部插入新元素。然而,在Java 8中,为了避免循环链表问题,扩容方法更改为尾插法。这意味着在插入新元素时,它会添加到链表的末尾,而不是链表头部。 为了避免循环链表问题,Java 8的HashMap在扩容时使用尾插法。当两个线程同时执行resize方法时,它们不会导致循环链表的产生,因为每次插入新元素时,它都会被放置在链表的末尾,而不是头部。

正文

1.底层结构

Java 7及之前版本

在Java 7及之前的版本中,HashMap的底层数据结构主要是数组加链表。具体实现如下:

  1. 数组:HashMap的核心是一个Entry数组(Entry<K,V>[] table),这个数组的大小总是2的幂。每个数组元素是一个单一的Entry节点,或者是一个链表的头节点。

  1. 链表:当两个或更多的键经过哈希运算后映射到数组的同一个索引上时,就会形成链表。Entry节点包含了键值对以及指向下一个Entry的引用,以此来解决哈希冲突。

Java 8及之后版本

从Java 8开始,HashMap的底层结构除了数组加链表之外,还引入了红黑树,以优化在链表过长时的查找性能。结构变为数组加链表加红黑树

  1. 数组:同样是一个Entry数组(Java 8中称为Node),大小仍然是2的幂,用于快速定位。
  2. 链表:在哈希冲突时,键值对仍会以链表形式链接在一起。但与Java 7不同的是,Java 8对链表的处理增加了转换为红黑树的条件。
  3. 红黑树:当一个桶中的链表长度超过8且HashMap的容量大于64时(不大于64会先对数组进行扩容resize()),链表会被转换成红黑树。这种转换提高了在大量哈希冲突情况下的查找效率,因为红黑树的查找时间复杂度为O(log n),相较于链表的O(n)有显著提升。

2.数据插入

在JDK1.7的时候,采用的是头插法

在JDK1.8改成了尾插法,这是因为头插法在多线程环境下扩容时可能会产生循环链表问题

线程不安全

无论是JDK1.7还是1.8都是线程不安全的,下图是1.8中的put方法

tab是就是HashMap的数组table,第一个if判断时做了赋值。框起来的部分表示如果没有hash冲突就直接在数组上插入元素,但是如果两个线程hash一样且都进入到了这个if下,线程1先执行的插入数据,线程2会覆盖1插入的数据。

那么什么是循环链表问题呢?这就不得不介绍一下HashMap的扩容机制了。

3.扩容机制

首先讲几个HashMap的属性

  • table:数组,存放链表或红黑树的头节点
  • Node:节点,属性有hash、key、value、next(下一个节点)
  • size:元素个数,即节点node个数,非数组长度
  • Capacity:当前数组长度
  • loadFactor:加载因子,默认为0.75f,简称loadFactor
  • threshold:扩容门槛,值为capacity*loadFactor,size达到这个门槛就扩容

当size大于threshold时,就会进行扩容resize()

扩容分为两步

  1. 创建一个新的数组,长度为原数组的两倍
  2. 遍历所有Node节点,重新计算index值(Java8首先会重新计算hash值),放到新数组里,存在hash冲突的就放到链表或红黑树

为什么要重新计算index值,直接把张三这条链表复制到新的数组中不行吗?

答案是不行的,因为index规则是根据数组长度来的,如图

所以index 的计算公式是这样的:

  • index = HashCode(key) & (Length - 1)

4.循环链表问题

循环链表问题理解起来比较麻烦,如果理解不了就知道JDK1.7HashMap扩容的时候有这么回事就行。但是可能是我自己太笨了万一大家一看就懂了呢

我们来看一下Java1.7扩容的源码

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);
}

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    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;
                //重新计算元素在数组中的索引
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

重点在于transfer方法,作用是重新计算index值然后将旧数组中的数据迁移到新数组

循环链表的产生:

原因:

假设我们有两个Thread都在执行resize方法,Thread1第一步刚执行完第23行Entry<K,V> next = e.next;就卡住了,这时Thread2执行完了resize方法。

过程:

  1. Thread1第一次执行完Entry<K,V> next = e.next后,e=张三,next=李四,也就是说第二步执行李四的插入
  2. Thread2执行完resize后,李四的next变成了张三
  3. 此时又回到Thread1,第二次执行Entry<K,V> next = e.next后,e=李四,next=张三,也就是说又要执行张三的插入,循环链表产生!

由此我们知道了循环链表产生的关键在于头部插入元素A时,元素A的next元素B的next是元素A本身,所以Java8采用了尾插法,避免了循环链表。

求赞!求关注!!以后会更新更多有用的内容!!!꒰⑅•ᴗ•⑅꒱

保佑大家代码永无bug ╰(´︶`)╯

与Java进阶:HashMap底层原理(通俗易懂篇)相似的内容:

Java进阶:HashMap底层原理(通俗易懂篇)

1.底层结构 Java 7及之前版本 在Java 7及之前的版本中,HashMap的底层数据结构主要是数组加链表。具体实现如下: 数组:HashMap的核心是一个Entry数组(Entry[] table),这个数组的大小总是2的幂。每个数组元素是一个单一的Entry节点,或者是一个链表的

【后端面经-Java】HashMap详解

本文详细介绍了hashmap,包括基本概念、hashmap数据结构、关键变量和重要方法,并且结合源码进行分析。

Java开发者的神经网络进阶指南:深入探讨交叉熵损失函数

在本文中,我们深入探讨了交叉熵函数作为一种重要的损失函数,特别适用于神经网络训练中。交叉熵通过衡量真实标签分布与模型预测分布之间的差异,帮助优化模型的性能。我们从信息论的角度解释了交叉熵的概念,它是基于Shannon信息论中的熵而来,用于度量两个概率分布之间的差异。

[转帖]Redis进阶(发布订阅,PipeLine,持久化,内存淘汰)

目录 1、发布订阅 1.1 什么是发布订阅 1.2 客户端实例演示 1.3 Java API演示 1.4 Redis发布订阅和rabbitmq的区别 2、批量操作 2.1 普通模式与 PipeLine 模式 2.2 适用场景 2.3 源码解析 2.4 Pipelining的局限性 2.5 事务与 L

Java进程 OOM的多种情况

Java进程 OOM的多种情况 摘要 OOM 其实有多种: 第一类是JVM原生自发处理的, 这种也分为多种情况. 1. 堆区使用了比较多,并且大部分对象都还有引用, GC不出来可用内存, 这是要给对象申请较大的内存空间时就会出现OOM的报错. 2. 除了IP 下一条命令指针的内存的区域, 其他任何区

[转帖]一次 Java 进程 OOM 的排查分析(glibc 篇)

https://juejin.cn/post/6854573220733911048 遇到了一个 glibc 导致的内存回收问题,查找原因和实验的的过程是比较有意思的,主要会涉及到下面这些: Linux 中典型的大量 64M 内存区域问题 glibc 的内存分配器 ptmalloc2 的底层原理 如

[转帖]一次 Java 进程 OOM 的排查分析(glibc 篇)

https://juejin.cn/post/6854573220733911048 遇到了一个 glibc 导致的内存回收问题,查找原因和实验的的过程是比较有意思的,主要会涉及到下面这些: Linux 中典型的大量 64M 内存区域问题 glibc 的内存分配器 ptmalloc2 的底层原理 如

[转帖]linux查看java堆栈信息_linux进程堆栈大小

https://www.cnblogs.com/cloudHui/p/17076184.html 1、查看JAVA进程JVM参数 jinfo -flags pid(进程号)-XX:CICompilerCount=2 最大的并行编译数-XX:InitialHeapSize=16777216 JVM 的

【转帖】Java Full GC (Ergonomics) 的排查

文章目录 1. Full GC (Ergonomics)1.1 Java 进程一直进行 Full GC1.2 Full GC 的原因1.3 检查堆占用 2. 代码检查3. 解决方式 1. Full GC (Ergonomics) 1.1 Java 进程一直进行 Full GC 例行检查线上运行的 J

[转帖]通过Shell脚本自动监控JAVA进程中线程cpu使用率

https://gitee.com/jialy/auto-monitor-java-process/tree/master 本文主要介绍在 show-busy-java-threads.sh 脚本的功能基础上,通过 process-cpu-monitor.sh 脚本实现Linux平台上Java进程或