【Java】Map(包括HashMap)

news2024/11/26 5:13:12

Map

  • HashMap 和 Hashtable 的区别
  • HashMap 和 HashSet 区别
  • HashMap 的底层实现
    • JDK1.8 之前
    • JDK1.8
      • 红黑树
    • HashMap 的长度为什么是 2 的幂次方
    • HashMap 多线程操作导致死循环问题
    • HashMap 为什么线程不安全?
  • ConcurrentHashMap
    • ConcurrentHashMap 和 Hashtable 的区别
    • JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

源码可以参考https://blog.csdn.net/cy973071263/article/details/123281532或者https://javaguide.cn/java/collection/hashmap-source-code.html

HashMap 和 Hashtable 的区别

  • 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  • 效率: 因为线程安全的问题,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 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。Hashtable 没有这样的机制。
    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
     public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }


HashMap 和 HashSet 区别

你看过 HashSet 源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

在这里插入图片描述

HashMap 的底层实现

在这里插入图片描述

JDK1.8 之前

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列

HashMap 通过 key 的 hashcode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突

所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

static int hash(int h) {
    // 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);
}

在这里插入图片描述

JDK1.8

当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。

    static final int hash(Object key) {
      int h;
      // key.hashCode():返回散列值也就是hashcode
      // ^:按位异或
      // >>>:无符号右移,忽略符号位,空位都以0补齐
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

在这里插入图片描述

红黑树

参考https://blog.csdn.net/cy973071263/article/details/122543826
性质:

  • 节点是红色或黑色。
  • 根是黑色。
  • 所有叶子都是黑色(叶子是NIL节点,是空节点!)。
  • 1个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
  • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点(简称黑高)。

红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。

此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。

HashMap 的长度为什么是 2 的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。

这个算法应该如何设计呢?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 **hash%length==hash&(length-1)**的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

HashMap 多线程操作导致死循环问题

JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap

HashMap 为什么线程不安全?

JDK1.7 及之前版本,在多线程环境下,HashMap 扩容时会造成死循环和数据丢失的问题。数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。
JDK 1.8 后,在 HashMap 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 HashMap 的 put 操作会导致线程不安全,具体来说会有数据覆盖的风险。
举个例子:

  • 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。
  • 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。
  • 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    // ...
    // 判断是否出现 hash 碰撞
    // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    // 桶中已经存在元素(处理hash冲突)
    else {
    // ...
}

ConcurrentHashMap

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要):
    • 在 JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁)每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
      在这里插入图片描述

    • 到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;

    • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。下面,我们再来看看两者底层数据结构的对比图。

在这里插入图片描述
ConcurrentHashMap 取消了 Segment 分段锁,采用 Node + CAS + synchronized 来保证并发安全。数据结构跟 HashMap 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。Java 8 中,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升

JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

  • 线程安全实现方式:JDK 1.7 采用 Segment 分段锁来保证安全, Segment 是继承自 ReentrantLock。JDK1.8 放弃了 Segment 分段锁的设计,采用 Node + CAS + synchronized 保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点。
  • Hash 碰撞解决方法 : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。
  • 并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

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

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

相关文章

常见面试题之JVM实践(调优)

1. JVM调优的参数可以在哪里设置参数值&#xff1f; 1.1 tomcat的设置vm参数 修改TOMCAT_HOME/bin/catalina.sh文件&#xff0c;如下图&#xff1a; JAVA_OPTS"-Xms512m -Xmx1024m" 1.2 springboot项目jar文件启动 通常在linux系统下直接加参数启动springboot项…

在Pandas中处理缺失数据

当没有为一个或多个项目或整个单元提供信息时&#xff0c;可能会出现数据缺失。缺失数据在现实生活中是一个非常大的问题。缺失数据在pandas中也可以称为NA&#xff08;不可用&#xff09;值。在DataFrame中&#xff0c;有时许多数据集只是缺少数据&#xff0c;因为它存在而未被…

52 # 二叉树的前中后遍历

二叉树的遍历 线性数据结构遍历比较简单&#xff0c;可以采用正序遍历、逆序遍历。 遍历树的目的一般是修改树&#xff0c;比如修改树的节点&#xff0c;采用访问者模式 前序遍历 前序遍历&#xff08;preorder traversal&#xff09;&#xff1a;先访问根节点&#xff0c;…

Go语言struct要使用 tags的原因解析

这篇文章主要介绍了为什么 Go 语言 struct 要使用 tags,在本文中&#xff0c;我们将探讨为什么 Go 语言中需要使用 struct tags&#xff0c;以及 struct tags 的使用场景和优势&#xff0c;需要的朋友可以参考下 在 Go 语言中&#xff0c;struct 是一种常见的数据类型&#xf…

由于找不到vcomp100.dll,无法继续执行代码,解决方法

为什么会由于找不到vcomp100.dll,无法继续执行代码问题呢&#xff1f; 文件被误删除&#xff1a;有时候&#xff0c;在进行系统清理或卸载应用程序时&#xff0c;可能会不小心删除了vcomp100.dll文件。如果某个程序依赖于该文件&#xff0c;并且文件被删除&#xff0c;那么该程…

ESP32开发:IDFV4.4配置LVGL8.3

配置LVGL8.3源码 LVGL GITHUB代码仓库如下&#xff1a;https://github.com/lvgl/lvgl/tree/release/v8.3 官方已经在ESP32上移植好的代码demo&#xff0c;目前最新版是LVGL 7.9&#xff1a;https://github.com/lvgl/lv_port_esp32 我们可以将LVGL官方配置好的ESP32 LVGL仓库下…

超详细的学习笔记:CSS盒子模型(附代码示例)

目录 一、CSS三大特性 1、继承性 2、层叠性 3、优先级 4、权重叠加的计算 二、PxCook的基本使用 三、盒子模型 1、盒子模型的介绍 2、内容的宽度和高度 3、边框 (border) 1、连写形式 2、单方向设置 3、单个属性 8、内边距&#xff08;padding&#xff09;和外边…

arm学习stm32之spi总线数码管倒计时

由于时间没有用时间计时器操作&#xff0c;有些误差&#xff0c;后续有空会翻新计时器版本 main.c #include "spi.h" extern void printf(const char *fmt, ...); void delay_ms(int ms) {int i,j;for(i 0; i < ms;i)for (j 0; j < 1800; j); } int num[10…

钉钉提示 redirect_url的域名不在appid的安全域名内

钉钉提示 redirect_url的域名不在appid的安全域名内 1、需要在《钉钉开放平台》- 开发者后台设置《钉钉扫码登陆功能》 2、如果钉钉界面没有钉钉扫码登陆功能-》点击浏览器右下角-》《返回旧版》 3、备注&#xff1a;当前访问的IP地址跟钉钉扫码登陆功能填写的IP地址需保持一致…

代码审计工具Fortify基本使用

最近接触到一款代码审计的工具 — Fortify SCA and Applications 22.2.0&#xff0c;现就其基本使用做一简单介绍&#xff01; Fortify是一个应用安全测试软件&#xff0c;是Micro Focus旗下AST&#xff08;应用程序安全测试&#xff09;产品。 Fortify能够提供静态和动态应用…

Acwing 853.有边数限制的最短路

Acwing 853.有边数限制的最短路 链接:853. 有边数限制的最短路 - AcWing题库 /* 题解:bellman_ford算法 可以算是一种暴力的算法了 他可以解决有复权边的单源最短路径 也可以解决图是否存在负环的问题 还可以求出 不超过k条边的最短路径问题 但是效率低下 时间复杂度为o(nk)n…

超有趣的linux命令2

超有趣的linux命令2 此次实验命令均在Ubuntu16.04版本上测试 注意有些命令需要在图形化界面才能显示效果 温馨提示&#xff1a;可能有人是第一次接触Ubuntu&#xff0c;所以下面详细写了如何配置源和网络&#xff0c;以及安装命令的方式 1. 首先配置软件源 以命令行方式配置…

Comate代码助手推出,现场生成了贪吃蛇游戏,我们距离AI自动编程还有多远?

Comate代码助手推出&#xff0c;现场生成了贪吃蛇游戏&#xff0c;我们距离AI自动编程还有多远&#xff1f; 百度智能云推出“Comate”代码助手&#xff0c;并正式开放邀测&#xff0c;不算很意外。 毕竟让AI写代码&#xff0c;跑一跑贪吃蛇&#xff0c;算是传统艺能。 不过你…

MongoDB 简介及安装(windows环境下)

一、MongoDB 简介 1、MongoDB 是什么 MongoDB 是一个开源的、基于分布式的、面向文档存储的非关系型数据库。是非关系型数据库当中功能最丰富、最像关系数据库的。 MongoDB 将数据存储为一个文档&#xff0c;数据结构由键值(key>value)对组成。MongoDB 文档类似于 JSON 对…

API全场景零码测试机器人——ATGen带来“超自动化”测试模式

HDC期间可参与新手入驻华为云Testplan抽奖活动&#xff0c;活动链接在文末 众所周知&#xff0c;软件服务及组件之间的交互主要依赖大量的API接口。以华为云300多个商用云服务为例&#xff0c;平均每个服务含500接口&#xff0c;接口总数高达10万&#xff0c;接口调用上下文业务…

[GWCTF 2019]babyvm 题解

虚拟机 这是一个虚拟机的题目 上图是虚拟机的执行流程&#xff0c;Dispatcher(分发器)读取Opcode&#xff08;虚拟机操作码&#xff09; 然后根据操作码进行跳转执行 所以做这道虚拟机的题&#xff0c;我们就要找到操作码 并且明白操作码对应的含义 然后对操作码进行一句一…

MySQL整合篇(SQL语句执行流程-->索引篇-->事务篇-->锁篇)

MySQL 基础篇 1.1 执行一条SQL语句会发生什么 1. MySQL架构一共分为两层 server 和 存储引擎层&#xff08;一般为Innodb引擎&#xff09; 主要执行流程都在server层&#xff1a;连接器&#xff0c;查询缓存&#xff0c;解析SQL&#xff08;解析器&#xff09;&#xff0c;执行…

MySQL存储函数和存储过程习题

创建表并插入数据 字段名 数据类型 主键 外键 非空 唯一 自增id INT 是 否 是 是 否name VARCHAR(50) 否 否 是 否 否glass VARCHAR(50) 否 否 是 否 否sch 表内容id name glass1 xiaommg glass 12 xiaojun glass 21、创建一个可以统计…

零代码编程:用ChatGPT批量识别图片PDF中的文字

有些PDF页面是图片格式&#xff0c;要怎么批量把图片中的文字识别出来&#xff1f;借助ChatGPT可以轻松完成这个任务。 首先要安装一些相关的软件和Python库。 安装tesseract-ocr&#xff08;OCR&#xff09;软件&#xff0c;最新版的是tesseract-ocr-w64-setup-v5.3.0.20221…

BP神经网络数据分类——语音特征信号分类(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f468;‍&#x1f4bb;4 Matlab代码 &#x1f4a5;1 概述 BP神经网络是一种常见的人工神经网络&#xff0c;用于数据分类和回归等任务。在语音特征信号分类中&#xff0c;BP神经网络可…