细说 Java 集合之 Map
前言:本文基于JDK8
一、HashMap
1.1、hash方法
hash
方法是map中的基石,后续很多操作都依赖hash
方法;
下面是 jdk 7 中 hash方法,注意
hashSeed
这个扰动因子,该值随机,所以同一个 key 每次调用hash方法后得到的hash值都不一样。
- 该值存在就是为了增加随机性,避免链表过长,性能急剧下降;
- jdk 8 中 hash方法移除了扰动因子,因为底层数据结构链表过长的情况下会进行转换为红黑树来提升性能;
// jdk 7
final int hash(Object k) {int h = hashSeed; // 扰动因子if (0 != h && k instanceof String) {return sun.misc.Hashing.stringHash32((String) k);}h ^= k.hashCode();// 扰动因子 参与 hash 计算// This function ensures that hashCodes that differ only by// constant multiples at each bit position have a bounded// number of collisions (approximately 8 at default load factor).h ^= (h >>> 20) ^ (h >>> 12);return h ^ (h >>> 7) ^ (h >>> 4);
}
1.2、put方法
1.3、putVal方法
其中( n - 1 ) & hash
是对 hash % n
(n 是 2 的幂次方)的性能优化。
HashMap数组长度都是2的幂次方,就是为了方便上述操作,提高处理效率。
注意上图中操作:数组位置
i
空闲时,直接存入节点,多线程情况下会导致元素丢失,所以putVal
方法线程不安全。
上图提到了采用拉链法解决hash冲突,下面来看下具体内容:
链表尾插法
JDK 7 时,采用的是头插法,即下一个冲突的键值对会放在上一个键值对的前面。扩容的时候就有可能导致出现环形链表,造成死循环。
注意链表转化为红黑树阈值TREEIFY_THRESHOLD 为 8;
TREEIFY_THRESHOLD - 1的值为 7
- 为何减1:
插入新节点时,binCount表示插入前的链表长度。
例如:
插入第8个节点时,插入前链表长度为7(binCount=7),满足条件7 >= 7,触发树化。
插入完成后,链表实际长度为8,此时已超过阈值,需转换为红黑树。
jdk 7 拉链法只有链表,并无红黑树处理逻辑。
1.4、方法treeifyBin
数组长度小于64时采用扩容数组来减少hash冲突;否则转换为红黑树;
1.5、扩容resize
重点关注迁移数组链表(红黑树同理,不再赘述)中元素:
之所以可以达到迁移前后数组位置的上述变化,奥秘就在于:
- 数组长度为2的 n 次幂;
- key 的 hash 值固定;
1.6、get方法
1.7、getNode方法
1.8、节点
1.8.1、Node
1.8.2、TreeNode
LinkedHashMap.Entry在HashMap.Node基础上扩展了
before
和after
指针,形成双向链表以维护插入/访问顺序。而TreeNode继承它后,天然具备了维护双向链表的能力。这种设计允许在树化(链表转红黑树)和反树化(红黑树转链表)时,快速重构链表结构,避免重新遍历节点。
1.9、小节
- JDK1.8 HashMap 结构为:数组+链表/红黑树;
- 数组长度为2的 n 次幂,方便用位运算快速计算数组下标;
- 链表转换为红黑树前提2个:
- 链表长度达到8个;
- 数组长度达到64;
- 数组扩容后,链表元素(红黑树一样)分为2个链表:
- 一个链表位置在新老数组中下标一样;
- 另外一个位置为旧数组位置+ 旧数组长度;
- 计算时通过hash值 与旧数组长度进行位与(&)操作来区分;
- 这种优化依赖hash值稳定不变和数组长度为2的n次幂;
二、LinkedHashMap
LinkedHashMap 继承了HashMap,增加了元素的顺序控制;
- 插入顺序;
- 访问顺序;
- 头部最老;
- 尾部最年轻;
2.1、HashMap 对LinkedHashMap支持:
HashMap 中添加了一系列钩子方法,来支持LinkedHashMap。
2.2、get方法
2.3、afterNodeAccess方法
- 开启访问顺序控制时,将访问节点移动到链表尾部;
- 迭代访问LinkedHashMap时,后输出的元素最近被访问过;
2.4、removeEldestEntry方法
- 是否移除最老元素;
- 当访问顺序标识开启,同时覆写此方法,如下图注释,即可实现简易版LRU(Least Recently Used,最近最少使用)
- 通过 LinkedHashMap实现LRU
// 自定义 LinkedHashMap,限制最大容量并开启淘汰逻辑
public class LRUCache<K,V> extends LinkedHashMap<K,V> {private final int maxCapacity;public LRUCache(int maxCapacity) {super(maxCapacity, 0.75f, true); // accessOrder=true 维护访问顺序this.maxCapacity = maxCapacity;}@Overrideprotected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return size() > maxCapacity; // 超过容量时移除最久未访问的节点}
}
2.5、afterNodeInsertion方法
LRU中移除最老元素就是此方法做的。
该钩子方法在HashMap中
putVal
方法结束前会调用;
其中的
removeNode
方法是HashMap中的方法,该方法结束前会调用钩子方法afterNodeRemoval
,详见2.6小节。
2.6、afterNodeRemoval方法
双向链表删除节点
三、ConcurrentHashMap
相对于 HashMap
,ConcurrentHashMap
就是线程安全的 map,其中利用了锁分段的思想大大提高了并发的效率。
ConcurrentHashMap
中与HashMap
中一致的内容不再赘述。
1.8 版本舍弃了 1.7中的 segment,使用了 synchronized
和 CAS
无锁操作来保证 ConcurrentHashMap
的线程安全。
- 为什么不用 ReentrantLock 而是 synchronzied 呢?
实际上,synchronzied 做了很多的优化(包括偏向锁、轻量级锁、重量级锁),synchronized 相较于 ReentrantLock 的性能其实差不多。
3.1、hash方法
ConcurrentHashMap 中hash方法名变为spread
,与HashMap相比,增加了将hash值变为正数(严谨点,非负数)的操作。
hash值为非负数是为了配合3.2小节中的辅助节点。
- key 不允许为 null,所以这里没有必要判空;
3.2、节点
ConcurrentHashMap 中5种节点分为两类:
- 数据节点(hash值为非负数,通过方法
spread
计算):- Node:链表节点;
- TreeNode:红黑树节点;
- 辅助节点(不存放数据,hash值固定,且均为负数):
- TreeBin:用于指向红黑树根节点,hash值为
-2
; - ForwardingNode:数组扩容时使用,hash值为
-1
; - ReservationNode:占位节点,hash值为
-3
,方法computeIfAbsent
和compute
中使用该节点。
- TreeBin:用于指向红黑树根节点,hash值为
3.2.1、Node
3.2.2、TreeNode
3.2.3、TreeBin
该节点hash值固定为TREEBIN = -2
3.2.4、ForwardingNode
- 注意,构造方法中
MOVED
是该节点的hash值,固定为-1
- 成员变量nextTable就是扩容过程中,用于指向新的数组位置。
3.2.5、ReservationNode
注意,构造方法中RESERVED
是该节点的hash值,固定为 -3
该节点使用详见3.6小节
3.3、put方法
为啥不允许key 、value 为null ?
多线程环境下的歧义性问题:
- HashMap中允许,当get(key) 返回null 时,单线程情况下:
- 通过
containskey(key)
可以确认key是否存在,即 key 不存在,所以value返回null; 或key存在,value就是null;- ConcurrentHashMap中不允许,它是面向多线程的,当get(key) 返回null 时:
- 通过
containskey(key)
无法确认key一开始是否存在?
- 线程A执行get(key) 返回null;
- 线程B执行put(key)设置,key = null或者删除了key;
- 线程A执行
containskey(key)
返回啥;问题来了,key一开始不存在(对于A就应该返回false),后面被B设置了,实际返回true;或者一开始存在(对于A就应该返回true),value就是null,后面被B删除了,实际返回false;这key一开始到底存不存在?已经产生歧义了。- 为了避免歧义,ConcurrentHashMap直接禁止key、value为null的情况。
3.4、putVal方法
注意比HashMap中的putVal
方法多了的循环是为了配合CAS操作。
CAS 一般都会伴随自旋操作,具体细节参阅 自旋、CLH、AQS浅析;
- 拉链法解决hash冲突,链表尾插法插入(链表长度超过8,尝试转换为红黑树)或者红黑树插入;
- 链表尝试转换为红黑树时,和HashMap一致,数组长度小于64时,首选扩容;否则,转换。
- 解决冲突时,使用内置锁
synchronized
,锁的对象时数组i
位置的节点,锁的粒度就是1个节点。
- jdk 7 使用
ReentrantLock
,锁的对象是segment
,包含数组相邻位置多个节点。- 为了避免浪费空间去为每一个数组桶关联一个锁对象,所以使用数组桶中第一个元素作为一把锁。
- 注意下图判断链表和红黑树的判断方法
- hash 值(下图变量
fh
)>=0 是链表;因为红黑树的根节点被节点TreeBin
(hash值固定为 -2)指着。
3.5、get方法
注意上图红黑树查找时,如遇到数组正在扩容时,会先找到
ForwardingNode
节点,通过ForwardingNode
找到扩容后的新数组,在新数组中查找(类似递归了)。
3.6、computeIfAbsent方法
我们只关注key不存在的情况,数组对应位置(记为
i
)元素为null,线程A、B同时执行此逻辑:
- 新建占位节点
ReservationNode
,记为r
, 通过CAS操作将占位节点设置设置到 数组i
;- 既然CAS,就会有失败的可能,所以外侧通过循环来自旋;
- CAS成功了,占位节点就会设置到数组
i
位置;后续就可以break
,退出循环;- CAS失败了,进入下次循环,注意下次循环进来后,不会再次进入CAS逻辑了;
- CAS失败意味着数组位置
i
元素(图示f
)!= null
,因为被别的线程设置了;- 下次循环会进入图示下方的同步代码块,此处加锁的对象是
f
;
- 此处加锁应该没有疑问,就是防止链表修改并发产生错误;
问题来了:占位节点r
作用域就在else if
这个代码块,每个线程执行时创建的r
都不同,为啥对r
加锁?
加锁一定有并发场景,问题是此处
r
会有其它线程来争抢吗?
- 回到上面CAS失败的情况,假设线程A CAS失败,线程B CAS成功;
- 线程B CAS成功意味着:线程B之前对自己创建的
r
加锁成功了;- 线程A 会进入下次循环,走到图示下方的同步代码块;
- 此时,线程B还在图示上方同步块中执行;
- 那线程A此时读到的对象是
f
就是线程B创建的r
了,即f == r
;- 线程A无法加锁成功,进入阻塞状态;
- 因为占位节点
r
时临时节点,最终会被线程B替换为真正的节点Node;
综上,占位节点ReservationNode
和真正的节点替换作为一个原子操作,是不可以分开的,所以此处必须对占位节点ReservationNode
进行加锁。
四、TreeMap
TreeMap可以保持元素的自然顺序,所以使用时需要提供排序器或将key实现排序接口。
4.1、树节点Entry
- TreeMap中的Tree是红黑树;
- 时间复杂度O(log n);
4.2、put方法
4.3、get方法
4.4、getEntry方法
相关文章:

细说 Java 集合之 Map
前言:本文基于JDK8 一、HashMap 1.1、hash方法 hash方法是map中的基石,后续很多操作都依赖hash方法; 下面是 jdk 7 中 hash方法,注意hashSeed 这个扰动因子,该值随机,所以同一个 key 每次调用hash方法后…...
【vue-echarts】——05.柱状图
文章目录 一、柱状图基本设置1.实现代码2.结果展示二、柱状图效果实现11.代码实现2.结果展示三、柱状图效果实现21.代码实现2.结果展示一、柱状图基本设置 柱状图:一种图表类型,因为构成是由一根一根类似柱子的数据条组合而成的坐标平面,所以命名为柱状 图。主要是用来反应对…...

【C】链式二叉树算法题1 -- 单值二叉树
leetcode链接https://leetcode.cn/problems/univalued-binary-tree/description/ 1 题目描述 如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。只有给定的树是单值二叉树时,才返回 true;否则返回 false。 示例 1࿱…...

C++11——智能指针和function库
目录 一、智能指针 1. std::unique_ptr(独占所有权指针) 2. std::shared_ptr(共享所有权指针) 3. std::weak_ptr(弱引用指针) 关键区别总结 最佳实践 基本用法 可封装的对象类型 核心特性 示例代码…...

[操作系统] 文件的软链接和硬链接
文章目录 引言硬链接(Hard Link)什么是硬链接?硬链接的特性硬链接的用途 软链接(Symbolic Link)什么是软链接?软链接的特性软链接的用途 软硬链接对比文件的时间戳实际应用示例使用硬链接节省备份空间用软链…...

RabbitMQ面试题及原理
RabbitMQ使用场景: 异步发送(验证码、短信、邮件…)MYSQL和Redis, ES之间的数据同步分布式事务削峰填谷 1. 消息可靠性(不丢失) 消息丢失场景: RabbitMQ-如何保证消息不丟失? 开启生产者确…...
SpringBoot中Get请求和POST请求接收参数详解
1、Get请求 1.1 方法形参接收参数 这种方式一般适用参数比较少的情况,并且前后端参数名称必须保持一致 RestController RequestMapping(“/user”) Slf4j public class DemoController { GetMapping("/query") public void getStudent(String name,Strin…...

分布式日志和责任链路
目录 日志问题 责任链问题 分布式日志 GrayLog简介 部署安装 收集日志 配置Inputs 集成微服务 日志回收策略 搜索语法 搜索语法 自定义展示字段 日志统计仪表盘 创建仪表盘 链路追踪 APM 什么是APM 原理 技术选型 Skywalking简介 部署安装 微服务探针 整合…...

h5 IOS端渐变的兼容问题 渐变实现弧形效果
IOS端使用渐变的时候有兼容问题 以下是问题效果,图中黑色部分期望的效果应该是白色的。但是ios端是下面的样子…… 安卓pc 支持: background-image: radial-gradient(circle 40rpx at 100% 0, #f3630c 40rpx, rgb(255, 255, 255) 50%);安卓pc ios支持…...

哈希算法--猜数字游戏
1.题目要求 输入两个位数相同的数,判断对应位置的数字是否相等,返回两个数。第一个数是数字和位置完全猜对的数字个数,第二个数是数字大小猜对但位置不对的数字个数 2.逐步编程 2.1 定义函数 def g(secret,guess):sec_dic{}gue_dic{}# 定义…...

idea生成自定义Maven原型(archetype)项目工程模板
一、什么是Maven原型(Maven archetype) 引自官网的介绍如下: Maven原型插件官网地址 这里采用DeepSeek助手翻译如下: Maven 原型 什么是原型? 简而言之,原型是一个 Maven 项目模板工具包。原型被定义为一…...

Redis面试常见问题——使用场景问题
目录 Redis面试常见问题 如果发生了缓存穿透、击穿、雪崩,该如何解决? 缓存穿透 什么是布隆过滤器? 缓存击穿 缓存雪崩 双写一致性(redis做为缓存,mysql的数据如何与redis进行同步呢?) …...
样式和ui(待更新)
element-plus 先在项目下执行安装语句执行按需导入的命令按照官方文档修改vitest.json sass样式定制 npm -i sass -D在项目下准备定制的样式文件 styles/element/index.scss(!注意这里是.scss文件在vitest.json 修改配置文件 Components({resolvers: [ElementPlusResolver(…...

大摩闭门会:250228 学习总结报告
如果图片分辨率不足,可右键图片在新标签打开图片或者下载末尾源文件进行查看 本文只是针对视频做相应学术记录,进行学习讨论使用...

线程(Thread)
一、概念 线程:线程是一个轻量级的进程 二、线程的创建 1、线程的空间 (1)进程的空间包括:系统数据段、数据段、文本段 (2) 线程位于进程空间内部 (3) 栈区独享、与进程共享文本段、…...
AI军备竞赛2025:GPT-4.5的“情商革命”、文心4.5的开源突围与Trae的代码革命
AI军备竞赛2025:GPT-4.5的“情商革命”、文心4.5的开源突围与Trae的代码革命 ——一场重塑人类认知边界的技术战争 一、OpenAI的“感性觉醒”:GPT-4.5的颠覆与争议 1.1 从“冷面学霸”到“温柔导师”:AI的情商跃迁 当用户输入“朋友放鸽子&…...

DeepSeek + 自由职业 发现新大陆,从 0 到 1 全流程跑通商业 IP
DeepSeek 自由职业 发现新大陆,从 0 到 1 全流程跑通商业 IP 商业定位1. 商业定位分析提示词2. 私域引流策略提示词3. 变现模型计算器提示词4. 对标账号分析提示词5. 商业IP人设打造提示词6. 内容选题策略提示词7. 用户人群链分析提示词8. 内容布局与转化路径设计提…...
Java进阶——常用工具类
日常开发中,Arrays、Collections 和 Objects 是非常实用的工具类,提供了丰富的功能,从而可以更高效地处理数组、集合和对象。本文将详细介绍这三个工具类的重要知识细节。 本文目录 一、 Arrays数组转集合并行排序优化Stream 支持 二、 Colle…...

【考试大纲】高级系统架构设计师考试大纲
目录 引言一、 考试说明1.考试目标2.考试要求3.考试科目设置二、 考试范围考试科目1:系统架构设计综合知识考试科目2:系统架构设计案例分析考试科目3:系统架构设计论文引言 最新的系统架构设计师考试大纲出版于 2022 年 11 月,本考试大纲基于此版本整理。 一、 考试说明…...
上位机知识篇---四种CPU架构交叉编译
文章目录 前言一、四种 CPU 架构1. x86/x86_64指令集位宽:应用场景编译工具 2. ARM指令集位宽:应用场景编译工具 3. MIPS指令集位宽应用场景编译工具 4. RISC-V指令集位宽应用场景编译工具 二、交叉编译1. 什么是交叉编译?定义应用场景 2. 交…...

IDEA运行Tomcat出现乱码问题解决汇总
最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...

label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...

无法与IP建立连接,未能下载VSCode服务器
如题,在远程连接服务器的时候突然遇到了这个提示。 查阅了一圈,发现是VSCode版本自动更新惹的祸!!! 在VSCode的帮助->关于这里发现前几天VSCode自动更新了,我的版本号变成了1.100.3 才导致了远程连接出…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
【算法训练营Day07】字符串part1
文章目录 反转字符串反转字符串II替换数字 反转字符串 题目链接:344. 反转字符串 双指针法,两个指针的元素直接调转即可 class Solution {public void reverseString(char[] s) {int head 0;int end s.length - 1;while(head < end) {char temp …...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
AspectJ 在 Android 中的完整使用指南
一、环境配置(Gradle 7.0 适配) 1. 项目级 build.gradle // 注意:沪江插件已停更,推荐官方兼容方案 buildscript {dependencies {classpath org.aspectj:aspectjtools:1.9.9.1 // AspectJ 工具} } 2. 模块级 build.gradle plu…...