多线程编程(12)之HashMap1.8源码分析

        之前已经分析过了一版1.7版本的HashMap,这里主要是来分析一下1.8HashMap源码。

一、HashMap数据结构

        HashMap 是一个利用散列表(哈希表)原理来存储元素的集合,是根据Key value而直接进行访问的数 据结构。

  •  JDK1.7 中,HashMap 是由 数组+链表构成的。

  •  JDK1.8 中,HashMap 是由 数组+链表+红黑树构成

 数组: 优势:数组是连续的内存,查询快(o1 )劣势:插入删除O(N) 链表: 优势:不是连续的内存,随 便插入(前、中间、尾部)插入O(1) 劣势:查询慢O(N)。

二、HashMap源码深度解析

2.1 成员变量与内部类

	// 默认数组容量,16,左移4位既16
	static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


	// 最大容量,左移30位,即2的30次幂
    static final int MAXIMUM_CAPACITY = 1 << 30;

	// 负载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

	// 链表转红黑树阈值
    static final int TREEIFY_THRESHOLD = 8;

    // 当链表的值小于6红黑树转链表
    static final int UNTREEIFY_THRESHOLD = 6;

	// 做红黑树转换的hashmap容量大小,如果前面单个链表个数大于8,但是hashmap容量小于64则直接扩容不做红黑树转换
    static final int MIN_TREEIFY_CAPACITY = 64;
	
	// hashmap得数组,中间状态数据
	 transient Node<K,V>[] table;

    // 用来存放缓存、中间状态数据
    transient Set<Map.Entry<K,V>> entrySet;

   
   // hashmap的实时数据
    transient int size;

	// 用来记录hashmap中K-V的修改次数
    transient int modCount;

    // 扩容临界值
    int threshold;

    // 负载因子
    final float loadFactor;
	

    // 具体存放数据的地方,JDK 1.7之前存放的是Entry,JDK1.8之后存放的是node节点
    static class Node<K,V> implements Map.Entry<K,V> {
		// hash值
        final int hash;
		// key值
        final K key;
        V value;
		// 下一个节点指针
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }
	
	

2.2 HashMap构造器

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }


    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }

    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);
    }

         使用默认构造函数时,在put之前和之后分别debug以上变量信息对比看看。

        第一次put之后:

  接下来我们使用自定义初始化参数验证:

         在有参数构造时,最终tableSizeFor。

    /**
	 * 带参数的初始化其实threshold就是调用这个函数
	 * 其实这里最主要的作用就是将cap转成n的指数倍
	 * 首先将n转成2进制,右移再和自己取或,相当于把里面所有的0变成1
	 * 最终的目的,找到>=n的,1开头后面全是0的数,例如n整数为17,那么二进制为10001 -1 =10000,经过不断右移或自己,那么高位都会变成了11111
	 * 最后n的值就为:11111+1=100000,对应的整数值就是32
	 */
	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;
    }

        总结:

  • 无参数构造时,容量为16,因子=0.75,第一次插入数据时,才会初始化table阈值等信息。
  • 有参构造函数,会取大于但是最接近你容量的2的指数倍。
  • 无论哪种构造方式,扩容阈值最终都是=容量*因子。

2.3 HashMap插入方法

        1)先了解以下流程图。

2)关于key做hash值的计算

        当我们调用put方法添加元素的时候,实际是调用了其内部的putVal方法,第一个参数需要对key求hash值。

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

         然后我们来看下hash是怎么取值的。

    static final int hash(Object key) {
        int h;
        // hash 扰动
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

图解:

结论:使用移位异或运算做第二次扰动,不是直接使用hashcode。

3)核心逻辑:

	/**
	 * onlyIfAbsent:true不更改现有值
	 * evict:fakse表示table为创建状态
	 */
	final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
		// 临时变量,tab=数组,p=插槽的指针,n=tab的长度,i为数组下标
        Node<K,V>[] tab; Node<K,V> p; int n, i;
		// 数组是否为null或者size长度=0,第一次put的时候初始化
        if ((tab = table) == null || (n = tab.length) == 0)
			// 初始化数组or扩容
            n = (tab = resize()).length;
		// 寻址,后续会将具体源码
        if ((p = tab[i = (n - 1) & hash]) == null)
			// 获取得到的坐标为空,则直接新的node放在插槽上
            tab[i] = newNode(hash, key, value, null);
        else {
			/**
			 * 如果存在有值那么说明存在hash碰撞了,需要追加成链表了
			 * e:是否找到与当前key相同的节点,找到说明是更新,null说明是新key插入
			 * k:临时变量,查找过程中的key在这里暂存
			 */
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // 如果第一个正好是相同的
                e = p; // 将p赋值给e,要注意此时还没有覆盖,只是单单的标记到了e,标记找到相同key的节点。
            else if (p instanceof TreeNode) // 是否是红黑树节点
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else { // 如果以上情况都不是,那么就是循环链表查询
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) { // 一直往链表后续查找,直到遍历到末尾
                        p.next = newNode(hash, key, value, null);
						// 一直遍历到最后判断找不到存在一样的key,那么直接插入到末尾,并且这里会判断是否需要转换成红黑树,内部会判断时候数据大小是否大于64,否则先做扩容
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break; // 遍历的过程如果找到一样的,那么赋值给e
                    p = e;
                }
            }
            if (e != null) { //如果e是非空的,那么说明前面循环中找到了一个跟当前key相同的值
                V oldValue = e.value;
				// 判断是否需要覆盖
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
		// 用来标记修改次数
        ++modCount;
		// 判断当前大小是否超过阈值,如果超过则做扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

4)寻址计算

        接着上面说的  i = (n - 1) & hash],这个寻址计算,我们来看下下面的例子:

其实在上面已经说到过了,我们的hash值是去hashcode然后跟本身右移16做异或运算得到最终扰动之后的hash值,然后这里跟对应的数组长度做与运算,hashcode不会超长了吗,我们来看下面的例子:

         其实可以看得到,其实就是拿hashcode的对应数组长度的低位做运算,hashcode超出那部分就不要了。

        就是不管你算出来的hash是多少,超出tab长度的高位会被抹掉,低位是多少就是你所在的槽的位置,也就是对应table的下标。

        这里可能有人会问了,为什么不做取模运算,取模也会保证不会超出来数组长度,其实这里做位运算的效率比取模运算是要高非常多的!!!!

2.4 hashmap扩容方法

        看下图:

 

        核心源码resize方法:

	 /**
	  * 这个方法包含了初试化以及扩容的方法
	  */
	final Node<K,V>[] resize() {
		// 原先的数组
        Node<K,V>[] oldTab = table;
		// 原数组长度,如果没有初始化那么就是0
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
		// 原扩容临界点
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) { // 如果原值就已经到达上线,不扩容直接返回
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
			/**
			 * 如果还没到上限,那么就重新计算新容量,注意这里还是没有开始迁移数据
			 */
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
			    // 原数组长度左移一位,其实就是*2
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // HashMap(int initialCapacity, float loadFactor)初始化的时候调用
            newCap = oldThr;
        else {               // HashMap() 初始化的时候调用,注意前面验证过了,是在第一次put的时候调的
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) { // 如果新阈值位空,那么则根据负载因子重新计算阈值
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
		
		
		// 以上各种操作就是各种容量相关值的计算,下面是计算之后重新迁移数据
        @SuppressWarnings({"rawtypes","unchecked"})
		// 按照新容量来创建新的数组
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 
        table = newTab;
		
		// 如果原数据不为空,则开始迁移数据
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) { // 遍历数组
                Node<K,V> e; // 临时变量,记录的是当前指向的node节点
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null; // 方便gc
                    if (e.next == null) // 不存在下一个节点,既只有一个节点,那么直接复制到新数组的索引下即可
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) // 如果是树节点,拆成两拼到新table上
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { 
						/**
						 * 如果是链表那么则拆成两个链表
						 * loHead:低位链表
						 * hiHead:高位链表
						 * 在上面我们说过了,其实我们是拿数组长度对应的hashcode的低位做与运算来得到对应的数组下标
						 * 现在数据扩容了两倍,就是就是多拿了一位hashcode对应高位来做运算,如果运算结果为0,那么代表hashcode的高一位为0,那么这node节点迁移到新数组上则是在低位
						 * 如果返回的是1,那么代表高位的是1,则迁移到新数组上面去这个节点应该是在高位
						 * 其实这也是为什么hashmap是2倍扩容的原因。
						 */
                        Node<K,V> loHead = null, loTail = null;
                        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;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.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; // 高位,原数据下标+原数组长度
                        }
                    }
                }
            }
        }
        return newTab;
    }

总结:

  • 扩容就是将旧表的数据迁移到新表。
  • 迁移过去的值需要重新计算hashcode,也就是他存储位置
  • 如果计算位置,采用低位链表和高位链表,如果位置下面的e.hash&oldCap等于0,那么它对应的就是低位链表,也就是数据位置保持不变。
  • e.hash & old不等于0就是要重写计算他的位置,也就是j+oldCap,就是高位链表位置。 例如原数组长度为16,16的二进制为:10000,原先计算hash是拿hashcode & (16-1)就是参与运算的二进制就是,1111,然后现在拿16来做与运算,就是判断原先的key的高一位是否是0,还是1,如果是1,那么与运算返回的结果就不是0,那么这个位置就是应该放在高位链表,否则表示这个key的原先的hashcode的高一位数为0,然后与运算返回的结果就是0,则应该放在低位链表。

2.5 HashMap获取方法

        

       

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MongoDB数据库清理策略: 自动化过期数据删除实战

1、引言 随着应用程序和业务数据的持续增长&#xff0c;有效地管理数据库存储空间成为维护系统性能的关键。在MongoDB这类NoSQL数据库中&#xff0c;定期清理过期数据变得尤为重要&#xff0c;这不仅能释放宝贵的存储资源&#xff0c;还能优化查询性能&#xff0c;确保数据库运…

[算法] 优先算法(三):滑动窗口(上)

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;Java …

Web安全技术期末考查-vulhub靶场搭建及漏洞复现

一、实验目的与要求 能根据报告找到难度适中的漏洞&#xff0c;搭建弱点环境&#xff0c;并验证该漏洞&#xff1b; 2.能给出该漏洞的修复建议。 二、实验原理与内容 漏洞原理 漏洞原理通常指的是计算机系统、软件、网络或其他技术系统中存在的安全缺陷&#xff0c;这些缺陷…

Ubuntu18 配置FFmpeg开发环境 (Vscode+CMake)

关于Vscode插件安装不再赘述&#xff0c;本文主要讲解如何配置FFmpeg的开发环境以及CMake文件写法&#xff0c;如果不知道该安装什么插件请看本文&#xff1a; Ubuntu配置Vscode 文章目录 1.安装FFmpeg开发包2.配置Vscode项目3.使用C语言验证FFmpeg版本 1.安装FFmpeg开发包 更新…

粉丝问,有没有UI的统计页面,安排!

移动应用的数据统计页面具有以下几个重要作用&#xff1a; 监控业务指标&#xff1a;数据统计页面可以帮助用户监控关键业务指标和数据&#xff0c;例如用户活跃度、销售额、转化率等。通过实时更新和可视化呈现数据&#xff0c;用户可以及时了解业务的整体状况和趋势。分析用…

深入剖析—【服务器硬件】与【Nginx配置】:从基础到实战

服务器硬件部分&#xff1a; Processor (CPU)&#xff1a;服务器的计算核心&#xff0c;负责处理数据和执行程序。Memory (RAM)&#xff1a;用于暂时存储和快速访问数据&#xff0c;决定了系统的运行速度和并发处理能力。Storage (HDD/SSD)&#xff1a;长期存储数据的设备&…

基于JT/T808、JT/T1078、苏标、粤标视频主动安全监控

1.概述 如下图是以实时视频点播与部标机产生了主动安全报警&#xff0c;各个服务之间的交互流程说明。 整个系统有以下几个核心组件组成&#xff1a; 1&#xff1a;系统业务端&#xff1a;车载监控业务系统&#xff0c;给用户提供车载监控整套业务流程与界面呈现&#xff1b;…

Docker安装Oracle11g数据库

操作系统&#xff1a;centOS9使用此方法检查是否安装Docker&#xff1a;docker --help&#xff0c;如果有帮助文件则证明安装成功使用此语句检查Docker是否正在运行&#xff1a;docker images&#xff0c;实际上是查看本地镜像如果发现未运行则开启Docker&#xff1a;systemctl…

rapidssl泛域名https600元一年

泛域名https证书也可以称之为通配符https证书&#xff0c;指的是可以用一张https证书为多个网站(主域名以及主域名下的所有子域名网站)传输数据加密&#xff0c;并且提供身份认证服务的数字证书产品。RapidSSL旗下的泛域名https证书性价比高&#xff0c;申请速度快&#xff0c;…

使用 FileZilla 在 Windows 和 Ubuntu 之间传文件

网线一端插在板子的WAN口上&#xff0c;另一段插在电脑上&#xff0c;然后要配一下板子的IP。 板侧&#xff1a; 使用串口链接板子与PC端&#xff1b; 输入指令 ifconfig eth0&#xff08;具体看wan口对应哪一个&#xff09; 192.168.1.99 PC端配置&#xff1a; 打开网络设…

操作系统实验:进程和线程同步和互斥(生产者消费者问题,睡觉的理发师问题)

1.生产者消费者问题&#xff08;信号量&#xff09; 参考教材中的生产者消费者算法&#xff0c;创建5个进程&#xff0c;其中两个进程为生产者进程&#xff0c;3个进程为消费者进程。一个生产者进程试图不断地在一个缓冲中写入大写字母&#xff0c;另一个生产者进程试图不断地…

sqlserver——查询(四)——连接查询

目录 一.连接查询 分类&#xff1a; 内连接&#xff1a; 1. select ... from A&#xff0c;B &#xff1b; 2. select ..from A&#xff0c;B where ..&#xff1b; 3.select ...,... from A join B on... 4. where 与 join...on 的区别 5. where位置的先后 导语&#xff1…

开发心电疾病分类的深度学习模型并部署运行于ARM虚拟硬件平台(AVH)

目录 一、ARM虚拟硬件平台介绍 二、心电疾病分类模型介绍 三、部署流程 3.1 基于百度云平台订阅虚拟硬件镜像 3.2 安装编译相关组件 3.3 数据加载 3.4 模型转换 方式一&#xff1a; tensorflow模型转换为onnx模型&#xff0c;onnx模型转换为TVM模型 方式二&#xff1…

【操作系统】发展与分类(手工操作、批处理、分时操作、实时操作)

2.操作系统发展与分类 思维导图 手工操作阶段&#xff08;此阶段无操作系统&#xff09; 需要人工干预 缺点&#xff1a; 1.用户独占全机&#xff0c;资源利用率低&#xff1b; 2.CPU等待手工操作&#xff0c;CPU利用不充分。 批处理阶段&#xff08;操作系统开始出现&#x…

从零入门激光SLAM(二十一)——FAST-LIO2论文解析

FAST-LIO2: Fast Direct LiDAR-Inertial Odometry 论文地址&#xff1a;https://ieeexplore.ieee.org/stamp/stamp.jsp?tp&arnumber9697912 代码&#xff1a;https://github.com/hku-mars/FAST_LIO 一、文章概述 1.问题导向 基于视觉传感器的高分辨率和高精度的实时密…

Excel 取出每组最后一行

Excel的前两列是两层的分组列&#xff0c;后两列是明细 ABCD1CM11112CM12123CM13134CM14145CM25156CM26167BM11218BM12229BM232310AM113111AM323212AM333313AM3434 现在要取出每小组的最后一行&#xff1a; ABCD1CM14142CM26163BM12224BM23235AM11316AM3434 使用 SPL XLL sp…

编译原理 期末复习笔记整理(上)

资料借鉴&#xff1a; 【编译原理】期末复习 零基础自学_哔哩哔哩_bilibili 编译原理笔记 第一章 引论 1.编译原理逻辑过程&#xff1a; 词法分析 语法分析 语义分析 中间代码生成 编译代码生成 2.词法分析 任务: 输入源程序&#xff0c;对…

SpringBootWeb 篇-深入了解 Mybatis 删除、新增、更新、查询的基础操作与 SQL 预编译解决 SQL 注入问题

&#x1f525;博客主页&#xff1a; 【小扳_-CSDN博客】 ❤感谢大家点赞&#x1f44d;收藏⭐评论✍ 文章目录 1.0 Mybatis 的基础操作 2.0 基础操作 - 环境准备 3.0 基础操作 - 删除操作 3.1 SQL 预编译 3.2 SQL 预编译的优势 3.3 参数占位符 4.0 基础操作 - 新增 4.1 主键返回…

不拍视频,不直播怎么在视频号卖货赚钱?开一个它就好了!

大家好&#xff0c;我是电商糖果 视频号这两年看着抖音卖货的热度越来越高&#xff0c;也想挤进电商圈。 于是它模仿抖音推出了自己的电商平台——视频号小店。 只要商家入驻视频号小店&#xff0c;就可以在视频号售卖商品。 具体怎么操作呢&#xff0c;需要拍视频&#xf…

Windows下mingw32编译ffmpeg5.1.4实现rtsp拉流

由于客户要求&#xff0c;要在Windows下使用mingw32编译&#xff0c;去ffmpeg.org下载需要编译的版本&#xff0c;使用msys2方法进行编译&#xff0c;使用QT5.10的编译器&#xff0c;基本上把网上的方法试了个遍&#xff0c;编译全部库总是报错出问题 查看了ffbuild文件夹中con…