HashMap多线程扩容导致死循环解析(JDK1.7)

2024-01-11 00:10

本文主要是介绍HashMap多线程扩容导致死循环解析(JDK1.7),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

前一篇 HashMap底层结构与实现原理 遗留了一个问题:JDK1.7中的HashMap在多线程情况下扩容可能会导致死循环。本篇就这个问题进行讲解。

扩容死循环

前一篇深入的讲解了HashMap1.7扩容的过程,这里回顾一下在扩容过程中,单链表的表现,相关的代码如下

void transfer(Entry[] newTable, boolean rehash) {int newCapacity = newTable.length;// 外层循环遍历数组槽(slot)for (Entry<K,V> e : table) {// 内层循环遍历单链表while(null != e) {// 记录当前节点的next节点Entry<K,V> next = e.next;if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}// 找到元素在新数组中的槽(slot)int i = indexFor(e.hash, newCapacity);// 用头插法将元素插入新的数组e.next = newTable[i];newTable[i] = e;// 遍历下一个节点e = next;}}
}

单线程情况下,假设A、B、C三个节点处在一个链表上,扩容后依然处在一个链表上,代码执行过程如下:
JDK1.7-HashMap扩容时链表转移过程
需要注意的几点是

  • 单链表在转移的过程中会被反转
  • table是线程共享的,而newTable是不共享的
  • 执行table = newTable后,其他线程就可以看到转移线程转移后的结果了

理解了单线程下链表在扩容时的行为,再来看多线程的情况就比较容易了

此处感谢评论区@伤神v同学的指点,以下多线程扩容图是修正后的图

还是关注transfer方法这段代码

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;  // *线程1在这行暂停(尚未执行)e = next;}}
}

HashMap扩容死循环

  • 线程1执行newTable[i] = e时暂停(未执行)
  • 线程2直接扩容完成
  • 线程1继续执行,此时线程1可以看到线程2扩容后的结果

图中已经画出了每一行代码执行后,HashMap的结构图,仔细观察图中的结构变化,就能理解为什么会死循环。

由此,完完整整的解释了为什么多线程情况下,JDK1.7版本的HashMap扩容有可能出现死循环。

JDK1.8改进

JDK1.8中扩容的方法是resize,对应的代码是(HashMap中第715行至第742行):

// 低位链表头节点,尾结点
// 低位链表就是扩容前后,所处的槽(slot)的下标不变
// 如果扩容前处于table[n],扩容后还是处于table[n]
Node<K,V> loHead = null, loTail = null;
// 高位链表头节点,尾结点
// 高位链表就是扩容后所处槽(slot)的下标 = 原来的下标 + 新容量的一半
// 如果扩容前处于table[n],扩容后处于table[n + newCapacity / 2]
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;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.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;
}

注意第12行的代码(e.hash & oldCap) == 0就可以判断,当前槽上的链表在扩容前和扩容后,所在的槽(slot)下标是否一致。举个例子:
假如一个key的hash值为1001 1100,转换成十进制就是156,数组长度为1000,转换成十进制就是8。

  1001 1100
& 0000 1000
--------------0000 1000

也就是(e.hash & oldCap) != 0,很容易计算出,扩容前这个key的下标是4(156 % 8 = 4),扩容后下标是12(156 % 16 = 12)即:12 = 4 + 16 / 2,满足n = n + newCapacity / 2,由此可以看出这种计算方式非常巧妙。至于第12行之后的代码就是基本的单链表操作了,只是一个单链表同时具有头指针尾指针,等到链表被分成高位链表和低位链表后,再一次性转移到新的table。这样就完成了单链表在扩容过程中的转移,使用两条链表的好处就是转移前后的链表不会倒置,更不会因为多线程扩容而导致死循环。

总结

本篇主要通过图解的方式,解释了为什么JDK1.7中的HashMap在多线程情况下扩容可能死循环,也解释了JDK1.8如何解决这个问题。不得不说,画图是个很好的分析方式,根据代码,一步一步把结构图画出来,比对着代码瞎琢磨效果好多了。

以上就是本篇文章的全部内容。

这篇关于HashMap多线程扩容导致死循环解析(JDK1.7)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/592534

相关文章

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

Java中HashMap的用法详细介绍

《Java中HashMap的用法详细介绍》JavaHashMap是一种高效的数据结构,用于存储键值对,它是基于哈希表实现的,提供快速的插入、删除和查找操作,:本文主要介绍Java中HashMap... 目录一.HashMap1.基本概念2.底层数据结构:3.HashCode和equals方法为什么重写Has

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

SysMain服务可以关吗? 解决SysMain服务导致的高CPU使用率问题

《SysMain服务可以关吗?解决SysMain服务导致的高CPU使用率问题》SysMain服务是超级预读取,该服务会记录您打开应用程序的模式,并预先将它们加载到内存中以节省时间,但它可能占用大量... 在使用电脑的过程中,CPU使用率居高不下是许多用户都遇到过的问题,其中名为SysMain的服务往往是罪魁