Map中的那些事

news2024/11/25 2:58:06

Map中的那些事

  • Map中的那些事
    • 拓展时间复杂度
      • O(1):常数级
      • O(logn):对数级
      • O(n):线性级
      • O(nlog n):线性对数级
      • O(n²):平方级
      • O(n³):立方级
      • O(2的n次方):指数级
    • hashMap
      • hashMap基本知识
        • 哈希冲突的定义
        • hashMap的实现原理
        • hashMap有四个构造器
      • 具体问题
        • HashMap的内部数据结构
        • HashMap允许空键空值吗
        • 影响HashMap性能的重要参数
        • HashMap的工作原理
        • HashMap中put()的工作原理
        • HashMap的底层数组长度为何总是2的n次方
        • JDK1.8中做了哪些优化?
        • HashMap线程安全方面会出现什么问题
        • HashMap线程安全方面会出现什么问题
        • 为什么1.8改用红黑树
        • 负载因子
        • HashMap中的equals()和hashCode()的都有什么作用?
        • HashMap和HashTable的区别有哪些?
        • 使用场景
    • ConcurrentHashMap
      • CAS算法
        • 源码实现
        • ABA
        • 开销

Map中的那些事

拓展时间复杂度

其实所谓的时间复杂度,空间复杂度都是数据结构的复杂度,反应程序执行时间随着输入规模的增大而增大的量级,很大程度上反应出算法性能的好坏,这个量级用大写的O加()表示

O(1):常数级

最低复杂程度,使用时间或使用空间与输入数据大小没有关系,无论输入数据多大,使用时间或者使用空间不变,哈希算法就是典型的常数级算法

O(logn):对数级

使用时间或空间随着输入数据增大,复杂度增大为log n倍,log n倍是n为2的几次方的上标值,二分查找就是对数级别算法

O(n):线性级

输入数据增大几倍,时间或空间增大几倍

大部分遍历就是线性级算法

O(nlog n):线性对数级

使用时间或空间随着输入数据增大,复杂度增大为nlogn倍,nlog n倍是n为2的几次方的上标值乘以n二分查找就是对数级算法

O(n²):平方级

输入数据增大几倍,时间或空间增大几的平方倍

冒泡排序就是平方级算法,不过复杂度是从O(n)->O(n²),冒泡排序在数据错位数量很小时适用

O(n³):立方级

输入数据增大几倍,时间或空间增大几的立方倍

O(2的n次方):指数级

输入数据增大几倍,时间或空间增大2的几次方倍

hashMap

我们都知道,要查找一个元素,如果是顺序查找,那么时间复杂度会达到O(n),比如:顺序表。也有可能达到O(logn),如:平衡树。那么要想更快,我们就可以考虑另外一种数据结构,他可以达到时间复杂度为O(1)。如HashMap

❓:什么是HashMap?

HashMap是一个key-value模型,具有映射关系,通过key值可以找到value值。并允许使用null值和null键,HashMap不保证映射的顺序。

hashMap基本知识

哈希冲突的定义

如果两个不同的元素,通过哈希函数得出的实际存储地址相同该如何❓

也就是我们对某个元素进行哈希运算,得到一个存储地址后,然后进行插入的时候,发现该位置已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞。由此可见哈希函数的设计至关重要,好的哈希函数会尽可能地保证计算简单和散列地址分布均匀,但是我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突

那么如何解决哈希冲突呢❓

哈希冲突的解决方案有多种:开放地址法(当发生冲突时,继续寻找下一块未被占用的存储地址,然后再放入元素即可),再散列函数法,链地址法,而hashMap即是采用了链地址法,也就是数组+链表的方式。

hashMap的实现原理

HashMap的主干是一个Entry数组。Entry是hashMap的基本组成单元,每个Entry包含一个key-value键值对。

🌵:HashMap的总体结构如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表。对于添加操作,其时间复杂度为O(n),首先遍历链表,存在即覆盖,否则新增;对于查找操作来讲,仍需遍历链表,然后通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现的越少,性能才会越好。
hashMap有四个构造器

其他构造器如果用户没有传入initialCapacity和loadFactor这两个参数,会使用默认值:initialCapacity默认为16,loadFactory默认为0.75

具体问题

HashMap的内部数据结构

数组+链表/红黑树

HashMap允许空键空值吗

HashMap最多只允许一个键为Null(多条会覆盖),但允许多个值为Null

影响HashMap性能的重要参数

初始容量:创建哈希表时桶的数量(数组的大小),默认为16

负载因子:哈希表在其容量扩容之前可以达到一种尺度,默认为0.75

HashMap的工作原理

HashMap是基于hashing的原理,我们使用put(key,value)存储对象到HashMap中,使用get(key)从HashMap中获取对象

HashMap中put()的工作原理

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

HashMap的底层数组长度为何总是2的n次方
  • 使用数据分布均匀、减少碰撞
  • 当length为2的n次方时,hash&(length-1)就相当于对length取模,而且在速度效率上比直接取模要快的多

⛵️这里可以使用逆向思维来解释这个问题,我们计算桶的位置完全可以使用h%length,如果这个length是随便设定值的话当然也可以,但是如果对它进行研究,设计一个合理的值的话,那么将对HashMap的性能发生翻天覆地的变化

  • ⚓️:当length为2的N次方的时候,h&(length-1)=h%length

    为什么&效率更高呢❓

    因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高

  • ⚓️:当length为2的N次方的时候,数据分布均匀,减少冲突

    此时我们基于第一个原因进行分析,此时hash策略为h&(length-1)。

  • ❗️当length为奇数时

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • ❗️当length为偶数时

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

从上面的图表中我们可以看到,当 length 为15时总共发生了8次碰撞,同时发现空间浪费非常大,因为在 1、3、5、7、9、11、13、15 这八处没有存放数据。

这是因为hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,那么最后一位为1的位置即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。这样,空间的减少会导致碰撞几率的进一步增加,从而就会导致查询速度慢。

而当length为16时,length – 1 = 15, 即 1111,那么,在进行低位&运算时,值总是与原来hash值相同,而进行高位运算时,其值等于其低位值。所以,当 length=2^n 时,不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

如果上面这句话大家还看不明白的话,可以多试一些数,就可以发现规律。当length为奇数时,length-1为偶数,而偶数二进制的最后一位永远为0,那么与其进行 & 运算,得到的二进制数最后一位永远为0,那么结果一定是偶数,那么就会导致下标为奇数的桶永远不会放置数据,这就不符合我们均匀放置,减少冲突的要求了。

JDK1.8中做了哪些优化?
  • 数组+链表改成了数组+链表或红黑树
  • 链表的插入方式从头插法改成了尾插法
  • 扩容的时候1.7需要对原数组中的元素进行重新hash,定位在新数组中的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小;
  • 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;
HashMap线程安全方面会出现什么问题
  • 在jdk1.7中,在多线程环境下,扩容时会造成环形链或数据丢失
  • 在jdk1.8中,在多线程环境下,会发生数据覆盖的情况
HashMap线程安全方面会出现什么问题
  • put的时候导致的多线程数据不一致,产生错误情况

    比如有两个线程 A和 B,首先 A希望插入一个 key-valul 对到HashMap 中,首先计算记录所要落到的 hash 桶的索引坐标,然后获取到该桶里面的链表头结点,而此时刚好线程 A 的时间片用完了,此时线程 B 被调度得以执行,和线程 A一样的执行过程 ,只不过线程 B 成功的将记录插到了桶里面(假设线程 A 插入的记录计算出来的 hash 桶索引和线程 B 要插入的记录计算出来的 hash 桶索引是一样的)那么当线程 B 成功插入之后, 线程 A 再次被调度运行时,它依然持有过期的链表头但是它对此一无所知(头插法),以至于它认为它应该继续采用头插法插入数据(它拿到的头节点刚好是刚刚线程B插入的节点的下一个节点),如此一来就覆盖了线程 B 插入的记录,这样线程 B 插入的记录就凭空消失了,造成了数据不一致的行为。
    
  • resize引起死循环

    这种情况发生在 HashMap 自动扩容时,当 2 个线程同时检测到元素个数超过 数组大小 ×负载因子 的时候。此时 2 个线程会在 put() 方法中调用resize() ,那么此时若两个线程同时修改一个链表结构会产生一个循环链表,接下来再想通过get()获取某一个元素,就会出现死循环。
    
为什么1.8改用红黑树

比如某些人通过找到你的hash碰撞值,来让你的HashMap不断地产生碰撞,那么相同key位置的链表就会不断增长,当你需要对这个HashMap的相应位置进行查询的时候,就会去循环遍历这个超级大的链表,性能及其地下。java8使用红黑树来替代超过8个节点数的链表后,查询方式性能得到了很好的提升,从原来的是O(n)到O(logn)。

负载因子

HashMap的默认负载因子为0.75,如果超过0.75,会重新resize一个原来长度两倍的HashMap,并且重新调用hash方法,对原来的数据进行重新的hash。

负载因子=存储的元素个数/数组的长度

HashMap中的equals()和hashCode()的都有什么作用?

通过key.hashCode()进行hashing,并计算下标(n-1&hash),从而获得buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或树中去查找对应的节点,另外也是为了保持数据一致性

HashMap和HashTable的区别有哪些?
  • HashTable是线程安全的,HashMap是非线程安全的
  • HashMap可以使用null作为key,不过建议还是尽量避免这样使用。HashMap以null作为key时,总是存储在table数组的第一个节点上。而HashTable则不允许null作为key
  • HashMap的初始容量为16,HashTable初始容量为11
  • HashTable计算hash是直接使用key的hashCode对Table数组的长度直接进行取模,HashMap计算hash对key的hashCode进行了二次hash。以获得更好的散列值,然后对table数组长度取模。
使用场景
  • 非并发场景使用HashMap,并发场景可以使用Hashtable,但是推荐使用CocurrentHashMap(锁粒度更低、效率更高)。
  • 另外在使用HashMap时要注意null值的判断,HashTable也要注意防止put null key 和null value。

ConcurrentHashMap

在包java.util.concurrent下,和HashMap不同的是专门处理,但是在很多地方是类似的,比如底层都是数组+链表+红黑树、数组大小都是2的幂次方等…

CAS算法

CAS可以看做是乐观锁的一种实现方式,Java原子类中的递增操作就是通过CAS自旋实现的

CAS全称Compare and Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

CAS涉及到三个属性:

  • 需要读写的内存位置V
  • 需要进行比较的预期值A
  • 需要写入的新值U

CAS具体执行时,当且仅当预期值A符合内存地址V中存储的值时,就用新值U替换掉旧值,并写入到内存地址V中。否则不做更新。

CAS算法图解

源码实现

CAS在JDK中是基于Unsafe类实现的,它是个跟底层硬件CPU指令通讯的复制工具类,源码如下:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

在JDK中使用CAS比较多的应该是AtomicXXX相关的类,我们以AtomicInteger原子整型类为例,分析CAS底层实现机制:

AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.incrementAndGet();

incrementAndGet是Unsafe类中的方法,它以原子方式对当前值加1,源码如下:

//以原子方式对当前值加1,返回的是更新后的值
 public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
 }
  • this指的是当前AtomicInteger对象
  • valueOffset是value的内存偏移量(new AtomicInteger()默认value值为0)
  • 1指的就是对value加1

Ps:最后还要+1是因为getAndAddInt方法返回的是更新的值,而我们要的是更新后的值

==valueOffset内存偏移量就是用来获取value的,==如下所示:

//获取unfase对象
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;//实际变量的值
    static {
        try {// 获得value在AtomicInteger中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

value是有volatile关键字修饰的,为了保证在多线程下内存可见性。

从static代码块可以看到valueOffset在类加载时就已经进行初始化了。

//var1-对象、var2-内存偏移量、var4-增加的值
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;//期望值
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        
        return var5;
    }
//根据偏移量获取value    
public native int getIntVolatile(Object var1, long var2);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

源码也不是很复杂,主要搞懂各个参数的意思,getIntVolatite方法获取到期望值value后调用compareAndSwapInt方法,失败则进行重试。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

对于上面的参数可以换个理解方式

unsafe.compareAndSwapInt(this, valueOffset, expect, update)
  • This:Unsafe对象本身,需要通过这个类来获取value的内存偏移地址。
  • valueOffset:value变量的内存偏移地址。
  • expect:期望更新的值
  • update:需要更新的最新值

如果原子变量中的value值等于expect,则使用update值更新该值并返回true,否则返回false。

ABA

CAS也不是万能的,它也存在着一些问题,比如ABA,我们来看看什么是ABA?

A–>B—>A 问题,假设有一个变量 A ,修改为B,然后又修改为了 A,实际已经修改过了,但 CAS 可能无法感知,造成了不合理的值修改操作。

为了解决这个 ABA 的问题,我们可以引入版本控制,例如,每次有线程修改了引用的值,就会进行版本的更新,虽然两个线程持有相同的引用,但他们的版本不同,这样,我们就可以预防 ABA 问题了。Java 中提供了 AtomicStampedReference 这个类,就可以进行版本控制了。

开销

CAS算法需要不断地自旋来读取最新的内存值,当长时间读取不到就会造成不必要的CPU开销。

Java 8推出了一个新的类LongAdder,他就是尝试使用分段CAS以及自动分段迁移的方式来大幅度提升多线程高并发执行CAS操作的性能!

简单来说就是如果发现并发更新的线程数量过多,就会开始施行分段CAS的机制,也就是内部会搞一个Cell数组,每个数组是一个数值分段。这时,让大量的线程分别去对不同Cell内部的value值进行CAS累加操作,这样就把CAS计算压力分散到了不同的Cell分段数值中了!

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

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

相关文章

MySQL的各种锁

1. MySQL有遇到过死锁的问题吗?你是如何解决的? 死锁,就是两个或两个以上的线程在执行过程中,去争夺同一个共享资源导致互相等待的现象,在没有外部干预的情况下,线程会一直处于阻塞状态,无法往下…

自动化办公篇之python批量改名

#批量命名 import xlwings as xw app xw.App(visibleFalse,add_bookFalse) workbook app.books.open("测试表.xlsx") for sheet in workbook.sheets:sheet.namesheet.name.replace("彩印之","银河") workbook.save() app.quit()

一篇文章带你用动态规划解决打家劫舍问题

动态规划的解题步骤可以分为以下五步,大家先好好记住 1.创建dp数组以及明确dp数组下标的含义 2.制定递推公式 3.初始化 4.遍历顺序 5.验证结果 根据打家劫舍的题意:两个直接相连的房子在同一天晚上被打劫会触发警报 所以我们制定出核心策略——偷东…

Generalizable NeRF in ICCV‘23

文章目录 前置知识Generalizable《Enhancing NeRF akin to Enhancing LLMs: Generalizable NeRF Transformer with Mixture-of-View-Experts》《WaveNeRF: Wavelet-based Generalizable Neural Radiance Fields》NeO 360: Neural Fields for Sparse View Synthesis of Outdoor …

习题2.18

有点烦了,改个次序做题发博文。今天先发2.18 题目很简单 将列表反序。用clojure来写,还是有点不习惯。忽然想起来,以前面试遇到过面试题,要求用递归函数对数组反序。原来就是想考察这些内容。 因为源语言已经有提供reverse&#…

2023最新接口自动化测试面试题

1、get和post的区别? l http是上层请求协议,主要定义了服务端和客户端的交互规格,底层都是tcp/ip协议 l Get会把参数附在url之后,用?分割,&连接不同参数,Get获取资源,post会把…

Python安装和环境配置教程

进官网根据不同的操作系统,下载适合自己的编译环境(在百度里直接输入Python) 选择安装包(我选择的是3.8.0版本) python官方下载目录中有好多种安装方式,一般情况选择Windows x86-64 executable installer …

酒水茶叶经营小程序商城的作用是什么

酒水茶叶的用户群体非常广,其价格从低到高覆盖了很多人群,对酒厂茶商或经销商来说,在市场高需求的同时,其所遇经营难题也比较明显。 通过【雨科】平台搭建酒水茶叶商城,实现商品线上自营售卖,电脑手机端小程…

缩短从需求到上线的距离:集成多种工程实践的稳定框架 | 开源日报 No.55

zeromicro/go-zero Stars: 25.7k License: MIT go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。 go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成…

【git】500 Whoops, something went wrong on our end.

在访问公的的git 时出现了500错误提示. 500 Whoops, something went wrong on our end. 哎呀,我们这边出了问题。 TMD 出了什么问题了???一脸懵逼。 登录git 服务器。 查看git的状态。 命令: gitlab-ctl statu…

【AI视野·今日Sound 声学论文速览 第二十四期】Thu, 12 Oct 2023

AI视野今日CS.Sound 声学论文速览 Thu, 12 Oct 2023 Totally 12 papers 👉上期速览✈更多精彩请移步主页 Daily Sound Papers Enhancing expressivity transfer in textless speech-to-speech translation Authors Jarod Duret LIA , Benjamin O Brien LIA , Yanni…

【C++】 局部对象,引用返回

1、new 关键字 会在堆内申请空间,如果仅仅是普通调用构造函数,不会在堆内开辟空间。 2、函数调用会形成栈帧,进行压栈操作,函数调用结束,会进行弹栈。 函数内的局部对象,会随着弹栈,而被销毁(…

关于github申请学生认证-卡在证明上传环节解决方案

在持有学信网英语翻译(30)某宝请人代注册(80) 通过github security log和聊天记录我大致猜想了下做法,前面的学校邮箱其实都好说主要是下面的那个上传照片的环节卡了我很久

生活空间中,餐桌该如何选择?福州中宅装饰,福州装修

餐桌设计 如何选择 不同的餐桌,定义不同的餐桌礼仪 在家的装修设计上, 很多人的关注点是这样的: 玄关收纳要强、客厅颜值要高、阳台功能要全、厨房要好用、卧室要舒适......餐厅、几把椅子一张长桌,够了吧。 餐厅说:“…

20.2 FMC驱动SDRAM的时序初始化实现及内存测试

继续上一篇的话题,写到SDRAM通过CubeMx配置后,在工程代码编写时直接引用的是我事先写好的时序初始化、内存测试文件,而未对其进行详细的解释,所以本篇文章就来娓娓道来。不多说,开始吧 SDRAM的初始化流程简述 SDRAM初…

在不同版本的linx编译erLang时出现./configure使用--prefix指定路径后,总在指定的另前多了/usr/local路径

昨天别的项目同事遇到一个编译遇到在不同linx版本下编译erLang的源码时,其中有一个版本的编译出现在./configure时加入---prefix指定编译后的安装目录,总会在指定的安装另前多了/usr/local的目录,导致无法源码安装到普通用户指定的目录 安装…

目录启示:使用 use 关键字为命名空间内的元素建立非限定名称

文章目录 参考环境三种名称非限定名称限定名称完全限定名称举个栗子 useuse 关键字use ... as .. 命名冲突真假美猴王两个世界 参考 项目描述搜索引擎Bing、GoogleAI 大模型文心一言、通义千问、讯飞星火认知大模型、ChatGPTPHP 官方PHP ManualPHP 官方language.namespaces.ra…

乐器经营商城小程序的作用是什么

乐器产品覆盖的人群非常广,小学生、老年人都有不小需求,也因此市场中的从业商家相对较多,产品丰富可供消费者选购,然而在实际经营中,线上线下面临痛点不少。 通过【雨科】平台搭建乐器小程序商城,将所有产品…

数据中心机房供电配电及能效管理系统设计

安科瑞虞佳豪壹捌柒陆壹伍玖玖零玖叁 摘要:现代的数据中心中都包括大量的计算机,对于这种场所的电力供应,都要求供电系统需要在所有的时间都有效,这就不同于一般建筑的供配电系统,它是一个交叉的系统,涉及…

力扣第108题 将有序数组转二叉搜索树 c++

题目 108. 将有序数组转换为二叉搜索树 简单 相关标签 树 二叉搜索树 数组 分治 二叉树 给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树…