【常用集合】深入浅出Map集合

HashMap

HashMap 主要用来存放键值对,它基于哈希表的 Map 接口实现,是常用的 Java 集合之一,是非线程安全的。

HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。

底层实现

通过 key 的 hashCode 经扰动函数处理得到 hash 值,并通过 hash & (length - 1) 确定存储位置(length为数组的长度)。若位置已有元素,比较 hash 值和 key,相同则覆盖,不同则用链表解决冲突。扰动函数用于优化 hashCode() 实现,减少哈希碰撞。

存储结构
  • 在 JDK 1.8 之前,HashMap 采用 链地址法 来解决哈希冲突,即将数组与链表结合使用。当多个元素的哈希值映射到同一个桶位时,HashMap 会在该桶位置创建一个链表,所有冲突的元素都以链表的形式存储。在插入新元素时,HashMap 使用 头插法,将新元素插入链表的开头位置,以此处理哈希冲突。
  • 在 JDK 1.8 之后,HashMap 解决哈希冲突的方式进行了优化,仍然采用 数组+链表 的结构,但引入了 红黑树 来提高性能。当某个桶位上的链表长度超过阈值(默认 8),并且哈希表桶的长度大于64时,链表会转换为红黑树,从而将链表的查询元素的时间复杂度由 O(n) 降低为 O(log n)。这样,当哈希冲突较多时,HashMap 仍然能够保持较高的查询和插入效率。
数据插入方式

在 JDK 1.7 及之前的版本中,HashMap 在多线程环境下进行扩容时,由于使用头插法重排链表,多个线程同时操作同一桶位的链表可能导致节点指向错误,形成环形链表,从而引发死循环。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。

长度取值

HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, HashMap 总是使用 2 的幂作为哈希表的大小。长度采用 2 的幂,是因为二进制位操作&相对于取模运算%能够提高效率。位运算直接对内存数据操作,不需要转换成10进制进行取模运算。

公式(hash % length == hash & (length - 1)成立的前提是Hash表的长度是 2 的 n 次方

并发安全问题

死锁问题

在 JDK 1.7 及之前的版本中,HashMap 在多线程环境下进行扩容时,由于使用头插法重排链表,多个线程同时操作同一桶位的链表可能导致节点指向错误,形成环形链表,从而引发死循环。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。

其他问题
  • 多线程put的时候,size的个数和真正的个数不一样
  • 多线程put的时候,可能会把上一个put的值覆盖掉
  • 当既有get操作,又有扩容操作的时候,有可能数据刚好被扩容换了桶,导致get不到数据
  • 和其他不支持并发的集合一样,HashMap也采用了fail-fast操作,当多个线程同时put和get的时候,会抛出并发异常

扩容机制

触发扩容

HashMap 中的容量阈值(threshold)是由当前数组容量与负载因子相乘得到的capacity * loadFactor。当 HashMap 中的元素数量超过这个阈值时,会触发扩容操作。默认的负载因子为 0.75,数组的初始容量为 16

扩容过程

在扩容过程中,HashMap 会创建一个新的数组,容量是原数组的两倍。所有原数组的元素都会被重新分配到这个新数组中。

节点迁移
  • 桶元素重新映射: 如果桶中只有一个元素,没有形成链表,则将原来的桶引用置为null,同时,将该元素进行rehash即可。
  • 链表重新链接: 扩容后的数据迁移实际上是部分的,一些元素保持原位置,而另一些元素则会被迁移到新的位置。这种优化减少了不必要的数据移动,提升了扩容操作的效率。
    1. 保持原位置:对于某个元素,如果 hash & oldLength == 0,那么该元素在扩容后的新数组中仍然保留在原位置。
    2. 移动到新位置:如果 hash & oldLength != 0,那么该元素在扩容后的新数组中的位置将从 index 变为 index + oldLength
  • 取消树化: 类似于上述链表的重新链接,当重新链接操作完毕后,会判断两个哈希桶上的链表长度是否小于等于6,如果满足条件,会将红黑树取消树化,退化成链表。

示例:旧哈希桶长度为8,有a、b、c、d四个元素,存储在下标为3的哈希桶上。

hash(a)=3;  hash(a)&7=3;
hash(b)=11; hash(b)&7=3;
hash(c)=27; hash(c)&7=3;
hash(d)=59; hash(d)&7=3;

如果扩容为16,由于hash(a)&8=0;无需移动,其他的不为零的元素移动到index + oldLength = 3 + 8 = 11

hash(a)=3;  hash(a)&8=0; hash(a)&15=3;
hash(b)=11; hash(b)&8=8; hash(b)&15=11
hash(c)=27; hash(c)&8=8; hash(c)&15=11
hash(d)=59; hash(d)&8=8; hash(d)&15=11

为什么HashMap的负载因子设置为0.75?

  • 性能和空间的平衡:
    • 负载因子过低(例如 0.5),意味着更频繁地扩容,虽然哈希冲突会减少,但空间利用率较低,增加了内存消耗。
    • 负载因子过高(例如 1.0),虽然提高了空间利用率,但增加了哈希冲突的概率,降低了查找和插入的效率。
  • 数学依据:
    • 负载因子选择为0.75(3/4),可以保证临界值 threshold = loacFactor * capacity为整数。

ConcurrentHashMap

底层实现

同HashMap
线程安全
  • JDK1.8之前:

    • 使用分段锁:在 JDK 1.7 及之前,ConcurrentHashMap 通过分段锁机制来实现线程安全。它将整个哈希表分为多个段(Segment),每个段内部维护一个独立的哈希表和锁。当对 ConcurrentHashMap 进行并发操作时,不同的线程可以同时操作不同的段,从而提高并发性。
    • Segment 是继承了 ReentrantLock 的子类:每个段都有自己独立的可重入锁,而操作的同步性依赖于 Segment 的锁机制。
  • JDK1.8之后:

    • 移除了分段锁:在 JDK 1.8 中,ConcurrentHashMap 不再使用分段锁。相反,它直接使用了一个Node<K, V>[]数组来存储键值对,并且通过更细粒度的锁无锁操作来实现线程安全。
    • CAS 操作:在插入和删除节点时,ConcurrentHashMap 使用 CAS(Compare-And-Swap)来确保并发安全。例如,当向一个空桶中插入第一个节点时,ConcurrentHashMap 会使用 CAS 操作直接插入,这样可以避免锁的使用。
    • Synchronized:在一些复杂操作(如链表或红黑树的插入或删除)中,ConcurrentHashMap 使用 synchronized 来锁定特定的桶(Node),而不是整个段或整个哈希表。这种方式虽然引入了锁,但锁的粒度非常小,只会锁定冲突的桶,因此可以大大减少锁竞争。
  • 并发度:

    • JDK1.8之前,最大并发度是Segment的个数,默认是 16。
    • JDK1.8之后,最大并发度是Node<K, V>[]数组的大小,并发度更大。

为什么key和value都不能为null?

null是一个特殊的值,表示没有对象或没有引用。

  • key为null:

    • 防止异常:如果键为 null,哈希计算过程中会抛出 NullPointerException
  • value为null:

    • 简化实现:在并发条件下无需对null值进行特殊处理,减少条件分支,提高可维护性。
    • 避免二义性:如果使用get(key)方法获取的value为null,则可能该 key 存在,但其对应的值是 null;或者该 key 不存在。

保证复合操作的原子性

ConcurrentHashMap 是线程安全的,这意味着它能够保证在多线程环境下对其进行并发读写操作时,不会出现数据不一致的情况。此外,它也避免了 JDK 1.7 及之前版本的 HashMap 中,在多线程操作时可能导致的 死循环问题。然而,线程安全 不等于 原子性,尤其是在涉及多个操作的情况下,ConcurrentHashMap 并不能确保所有的复合操作都是原子性的

复合操作: 是指由多个基本操作(如 putgetremovecontainsKey 等)组合而成的操作。

例如:如果多个线程同时调用 containsKey 进行检查并随后调用 put,在第一个线程判断某个键不存在时,另一个线程可能已经插入了该键,这样第一个线程的操作就会覆盖另一个线程的结果,导致数据不一致。

为了解决复合操作的原子性问题,ConcurrentHashMap 提供了一些内置的原子复合操作方法。这些方法能够确保整个操作过程在并发环境下是原子性的,即要么全部成功,要么全部失败,不会出现中间状态。

  • putIfAbsent(K key, V value)

    • 如果指定的键 key 尚未存在于 ConcurrentHashMap 中,则将其对应的值设置为 value。该操作是原子性的,能够确保在并发情况下,只有第一个插入的线程成功,后续插入将被忽略。
    • 这种操作特别适合用于避免重复插入。
  • computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)

    • 只有当指定的键 key 存在时,才会根据提供的 remappingFunction 计算新值并更新。如果键不存在,则不执行任何操作。该方法同样确保了计算和更新的原子性。

Map对比

HashMap和Hashtable的区别

  • 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。
  • 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;
  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
  • 初始容量大小和每次扩充容量大小的不同: ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。

HashMap和HashSet的区别

HashSet 底层就是基于 HashMap 实现的。

HashMapHashSet
实现了 Map 接口实现 Set 接口
存储键值对仅存储对象
调用 put()向 map 中添加元素调用 add()方法向 Set 中添加元素
HashMap 使用键(Key)计算 hashcodeHashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性

HashMap和TreeMap的区别

TreeMapHashMap 都继承自AbstractMap ,但是需要注意的是TreeMap它还实现了NavigableMap接口和SortedMap 接口。

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。

ConcurrentHashMap 和 Hashtable 的区别

底层数据结构

  • JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样采用数组+链表/红黑二叉树
  • Hashtable 一直采用的是 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;

实现线程安全的方式(重要)

  • ConcurrentHashMap:

    • 移除了分段锁:在 JDK 1.8 中,ConcurrentHashMap 不再使用分段锁。相反,它直接使用了一个Node数组来存储键值对,并且通过更细粒度的锁无锁操作来实现线程安全。
    • CAS 操作:在插入和删除节点时,ConcurrentHashMap 使用 CAS(Compare-And-Swap)来确保并发安全。例如,当向一个空桶中插入第一个节点时,ConcurrentHashMap 会使用 CAS 操作直接插入,这样可以避免锁的使用。
    • Synchronized:在一些复杂操作(如链表或红黑树的插入或删除)中,ConcurrentHashMap 使用 synchronized 来锁定特定的桶(Node),而不是整个段或整个哈希表。这种方式虽然引入了锁,但锁的粒度非常小,只会锁定冲突的桶,因此可以大大减少锁竞争。
  • Hashtable:

    • 使用全表锁(Synchronized)Hashtable 通过对每个操作(如 putgetremove 等)加上 synchronized 来保证线程安全,但它使用的是全表锁,导致每次操作都必须锁住整个对象。这会导致多个线程在访问不同元素时也会被阻塞,增加锁竞争,降低并发性能。
    • 性能瓶颈:全表锁机制使得即使线程操作不同的键,仍会被串行化。在高并发场景中,锁的竞争严重影响吞吐量,导致性能大幅下降。

如何解决Hash冲突?

1.开放定址法

  • 开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
  • 常见的开放寻址技术有线性探测、二次探测和双重散列。
  • 这种方法的缺点是可能导致“聚集”问题,降低哈希表的性能。

2.链地址法

  • 每个哈希桶(bucket)指向一个链表。当发生冲突时,新的元素将被添加到这个链表的末尾。

  • 在Java中,HashMap就是通过这种方式来解决哈希冲突的。Java8之前,HashMap使用链表来实现)

  • 从Java 8开始,当链表长度超过一定阈值时,链表会转换为红黑树,以提高搜索效率。

3.再哈希法

  • 当哈希地址发生冲突用其他的函数计算另一个哈希函数地址,直到冲突不再产生为止。
  • 这种方法需要额外的计算,但可以有效降低冲突率。

4.建立公共溢出区

  • 将哈希表分为基本表和溢出表两部分,发生冲突的元素都放入溢出表中。

5.一致性hash

  • 主要用于分布式系统中,如分布式缓存。它通过将数据均匀分布到多个节点上来减少冲突

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1538584.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

C# 找到给定点集的简单闭合路径(Find Simple Closed Path for a given set of points)

给定一组点&#xff0c;将这些点连接起来而不相交 例子&#xff1a; 输入&#xff1a;points[] {(0, 3), (1, 1), (2, 2), (4, 4), (0, 0), (1, 2), (3, 1}, {3, 3}}; 输出&#xff1a;按以下顺序连接点将 不造成任何交叉 {(0, 0), (3, …

前端分段式渲染较长文章

实现思路&#xff1a; 1. 后端返回整篇文章。 2. JavaScript 分段处理&#xff1a;将文章按一定的字符或段落长度分割&#xff0c;然后逐步将这些段落追加到页面上。 3. 定时器或递归调用&#xff1a;使用 setInterval 或 setTimeout 来控制段落的逐步渲染。 代码实现示例 …

Linux(6)--CentOS目录

文章目录 1. 根目录2. cd目录切换命令3. CentOS目录介绍4. pwd命令介绍5. ls命令介绍5.1 ls5.2 ls -a5.3 ls -l 1. 根目录 Windows电脑的根目录是计算机(我的电脑)&#xff0c;然后C盘、D盘。 Linux系统的根目录是/&#xff0c;我们可以使用cd /进入根目录&#xff0c;然后使…

Java访问一口气讲完!o(*≧▽≦)ツ┏━┓

Java this关键字 Java面向对象设计 - Java this关键字 什么是 this&#xff1f; Java有一个名为 this 的关键字。它是对类的当前实例的引用。 它只能在实例的上下文中使用。 以下代码显示如何使用this关键字。 public class Main {int varA 1;int varB varA; // Assign …

深入探索Docker核心原理:从Libcontainer到runC的演化与实现

随着容器技术的发展&#xff0c;Docker从早期的Libcontainer逐步演化到runC&#xff0c;推动了容器运行时的标准化进程。Libcontainer是Docker容器的核心管理工具&#xff0c;而runC则在此基础上发展成为符合OCI&#xff08;Open Container Initiative&#xff09;标准的轻量级…

8.2Roberts算子边缘检测

基本概念 Roberts算子是一种简单的一阶导数边缘检测算子&#xff0c;它通过计算图像在水平和垂直方向上的梯度来检测边缘。在OpenCV中&#xff0c;Roberts算子可以通过手动应用卷积核来实现。Roberts算子是一组2x2的小型滤波器&#xff0c;用于检测图像中的垂直和水平边缘。 …

GEE 案例:利用sentinel-2数据计算的NDVI指数对比植被退化情况

目录 简介 NDVI指数 数据 函数 ui.Chart.image.series(imageCollection, region, reducer, scale, xProperty) Arguments: Returns: ui.Chart 代码 结果 简介 利用sentinel-2数据计算的NDVI指数对比植被退化情况 NDVI指数 NDVI&#xff08;Normalized Difference Ve…

遥感图像目标检测数据集-DOTA数据集

DOTA数据集(v1.0版本和v1.5版本)&#xff0c;训练集1411张&#xff0c;验证集458张&#xff0c;测试集若干&#xff0c;共16种类别。数据集图片大小不一&#xff0c;需要进行裁剪&#xff0c;可设置裁剪重叠大小以及裁剪图片大小。此处按照默认参数裁剪&#xff0c;重叠200像素…

二极管选型

稳压二极管&#xff08;齐纳二极管&#xff09; 肖特基二极管 发光二极管 TVS二极管

记录一下ElementUI 3 在浏览器导入, table表格显示问题

当时问题忘了截图, 现在通过文字记录一下问题 我直接在html了引入 vue3 和 ElementUI 3 , 使用了table组件, 但是表格的td 总是只显示一列, 问题是我的 el-table-column 标签 没有结束标签 , 在vue文件模块化里写不需要结束标签, 在浏览器里无法直接识别出来, 所以他是渲染了第…

基于yolov8的肉鸡健康状态检测系统python源码+onnx模型+评估指标曲线+精美GUI界面

【算法介绍】 基于YOLOv8的肉鸡健康状态检测系统是一个先进的目标检测应用&#xff0c;旨在通过图像分析实现对肉鸡健康状态的快速、准确评估。该系统利用了YOLOv8模型的尖端技术&#xff0c;该模型由Ultralytics公司开发&#xff0c;具有卓越的检测精度和速度。 YOLOv8模型采…

C++---类与对象一

类的定义 class className{//成员字段//成员函数 };class定义类的关键字&#xff0c;className是自己决定的类名&#xff0c;{ } 为类的主体&#xff0c;花括号里是类的内容。类的内容大致分为类的成员属性&#xff08;变量&#xff09;和类的成员函数。注意定义类后面需要跟;…

理解人工智能、机器学习与深度学习的关系

1. 人工智能&#xff08;AI&#xff09;宏观的智能概念 人工智能&#xff08;Artificial Intelligence, AI&#xff09;是一个广泛的领域&#xff0c;涉及设计和开发能够表现出智能行为的计算机系统。这些系统可以模拟或执行类似于人类的认知功能&#xff0c;如学习、推理、决…

react 路由 react-router/react-router-dom

react-router-dom中包含react-router 安装前者即可 npm install react-router-dom -Simport { BrowserRouter as Router, Route, Link, Switch } from react-router-dom <Switch>组件&#xff0c;和switch语法一样&#xff0c;遇到匹配就结束&#xff0c;后面的<Route…

如何全面优化MySQL性能

MySQL数据库性能优化是一项复杂而细致的任务&#xff0c;它涉及到数据库设计、查询优化、服务器配置等多个方面。以下是几个关键的步骤和策略&#xff0c;旨在帮助提升MySQL数据库的运行效率&#xff1a; 优化数据库设计 选择合适的数据类型&#xff1a;合理选择数据类型不仅能…

树——数据结构

这次我来给大家讲解一下数据结构中的树 1. 树的概念 树是一种非线性的数据结构&#xff0c;它是由n(n>0&#xff09;个有限结点组成一个具有层次关系的集合。 叫做树的原因&#xff1a;看起来像一棵倒挂的树&#xff0c;根朝上&#xff0c;叶朝下。 特殊结点&#xff1a…

vmware中的ubuntu系统扩容分区

1.虚拟机关机 右击虚拟机/设置&#xff0c;进入虚拟机设置 3.启动虚拟机&#xff0c;进入命令行 4.fdisk -l查看要扩展的分区名 5.resize要扩容的分区 su root parted /dev/sda resizepart 3 100% fdisk -l resize2fs /dev/sda3 df -T完成 6.其他 进入磁盘管理 fdisk /d…

Doker学习笔记--黑马

介绍&#xff1a;快速构建、运行、管理应用的工具 在不同的服务器上部署多个应用&#xff0c;但是往往不同应用之间会有冲突&#xff0c;因为它们所依赖的环境&#xff0c;函数库&#xff0c;配置都不一样&#xff0c;此时docker在运行时形成了一个隔离环境&#xff08;容器&am…

idea上传jar包到nexus

注意&#xff1a;确保idea中项目为maven项目&#xff0c;并且在nexus中已经创建了maven私服。 1、配置pom.xml中推送代码配置 <distributionManagement> <repository> <id>releases</id> <url>http://127.0.0.1:8001/repository/myRelease/<…

c++9月18日

1&#xff0c;斐波那契数列 int str 0;int num 0;int kgo 0;int qto 0;cout<<"请输入字符串";string str1;getline(cin,str1);for(int i0;i<(int)(str1.size());i){if((str1.at(i)>65&&str1.at(i)<90)||(str1.at(i)>97&&str1.…