2-2-3-9-1-1、jdk1.7HashMap详解

news2024/11/20 3:18:05

目录

  • 数据结构
    • 链表的作用
    • 链表问题
    • 数据结构简图
  • 源码解析
    • 重要成员变量说明
    • 构造函数
    • put操作
      • 初始化数组
      • Key为null的处理
      • 计算hash
      • 添加链表节点--新增Entry
        • 扩容
  • 缺点
  • 扩容死锁分析
    • 单线程扩容
    • 多线程扩容

数据结构

jdk1.7的hashmap的底层结构是数组加单向链表实现的。将key的hash值进行取模获取index既即将存放的元素的数组的位置。然后到对应的链表中进行put和get操作

链表的作用

因为对数组进行取模的时候可能会遇到获取index的位置是一样的,所以可能会遇到hash碰撞冲突,而HashMap使用链表来存储hash碰撞的元素

链表问题

如果在hash碰撞发生严重的情况下,则会出现插入和获取元素时间过长的问题,jdk1.8对此进行了树化来解决这个问题,详情见jdk1.8HashMap的详解

数据结构简图

在这里插入图片描述


数组(绿色):hash数组(桶),数组元素是每个链表的头节点
链表(浅蓝色):解决hash冲突,不同的key映射到了数组的同一索引处,则形成链表

源码解析

重要成员变量说明

//默认初始化容量 16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认负载因子  超过 容量*负载因子  就需要扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//默认空数组
static final Entry<?,?>[] EMPTY_TABLE = {};

//用来存放元素得数组,默认== EMPTY_TABLE 用来判断hashMap是否真正的初始化
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

//元素数量
transient int size;

//构造函数时为设置的容量,初始化后为容量*负载因子
int threshold;

//负载因子
final float loadFactor;

为什么负载因子的大小默认为0.75而不是1呢
这涉及到一个数学运算
根据HashMap的扩容机制,他会保证capacity的值永远都是2的幂
那么,为了保证负载因子(loadFactor) * 容量(capacity)的结果是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的幂乘积结果都是整数
如果我们把负载因子设置成1,容量使用默认初始值16,那么表示一个HashMap需要在"满了"之后才会进行扩容,显然会发生更多的hash碰撞
总结:负载因子默认为0.75是最适合减少hash碰撞又能保证 loadFactor*capacity的结果是一个整数的一个值

构造函数

public HashMap() {
    //调用下面的构造函数  16, 0.75F
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//                容量                    负载因子
public HashMap(int initialCapacity, float loadFactor) {
    //容量不能小于0
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    //容量最大为MAXIMUM_CAPACITY
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    //设置当前负载因子为默认负载因子
    this.loadFactor = loadFactor;
    //构造函数时threshold 为 容量
    threshold = initialCapacity;
    //空方法
    init();
}

put操作

public V put(K key, V value) {
    //如果table == EMPTY_TABLE则table没有真正的初始化,需要初始化
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }

    //hashmap 可以存放null key
    if (key == null)
        return putForNullKey(value);

    //计算key的hash
    int hash = hash(key);

    //求出当前key要存放在数组中的下标
    int i = indexFor(hash, table.length);

    //遍历当前下标的key, 看有没有相同的key, 如果有就替换value,返回oldValue
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            //空方法
            e.recordAccess(this);
            return oldValue;
        }
    }

    //检测并发修改
    modCount++;

    //说明当前位置没有相同的key,新增Entry放入链表
    addEntry(hash, key, value, i);
    //存放前key不存在 返回null作为oldValue
    return null;
}

初始化数组

private void inflateTable(int toSize) {
    //hashMap为了方便计算下标中的位置,所以容量需要设置为2的次方,但是没有做强制校验
    //如果设置的容量不是2的次方,这里会获取到比容量大 且 最小的2的次方
    int capacity = roundUpToPowerOf2(toSize);

    //获取扩容的门槛,容量 * 负载因子
    threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);

    //初始化数组为2的次方的capacity
    table = new Entry[capacity];

    //为了看一下capacity是否需要重置一下哈希种子,是为了得到的哈希值更加散列。
    initHashSeedAsNeeded(capacity);
}

Key为null的处理

private V putForNullKey(V value) {
    //如果key==null,在1.7中就会把该元素放入下标为0的位置
    //如果0位置已经存在元素,就需要判断是否是同一个key,如果是,就需要替换oldValue并返回
    //不是null的元素求得的位置也可能是0
    for (Entry<K,V> e = table[0]; e != null; e = e.next) {
        if (e.key == null) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }
    modCount++;

    //此时key==null,这一步表示0位置没有key==null的Entry,新增Entry入链表
    addEntry(0, null, value, 0);
    return null;
}

计算hash

 final int hash(Object k) {
    int h = hashSeed;
    if (0 != h && k instanceof String) {
        return sun.misc.Hashing.stringHash32((String) k);
    }

    h ^= k.hashCode();

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

 static int indexFor(int h, int length) {
    // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
    return h & (length-1);
}

添加链表节点–新增Entry

void addEntry(int hash, K key, V value, int bucketIndex) {
    //判断是否扩容 
    //1.当前size >= capacity * loadFactory
    //2.当前下标位置不等于null
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容,容量必须是2的次方,所以扩容是旧容量的2倍
        resize(2 * table.length);
        //重新计算在扩容之后hash
        hash = (null != key) ? hash(key) : 0;
        //重新计算在扩容之后的下标
        bucketIndex = indexFor(hash, table.length);
    }

    //放入Entry
    createEntry(hash, key, value, bucketIndex);
}

void createEntry(int hash, K key, V value, int bucketIndex) {
    //获取到当下下标的entry
    Entry<K,V> e = table[bucketIndex];
    //采用头插法放入链表头部,e是当前下标的元素,下面Entry的构造函数时next
    //即把当前下标的e作为新元素的next,把next放到当前下标位置
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}

扩容

void resize(int newCapacity) {
    //临时保存旧table
    Entry[] oldTable = table;
    //临时保存旧table的长度
    int oldCapacity = oldTable.length;
    //如果旧容量已经达到了最大容量,不扩容
    if (oldCapacity == MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return;
    }

    //创建扩容后的数组
    Entry[] newTable = new Entry[newCapacity];

    //把旧数据拷贝到新数组,  initHashSeedAsNeeded(newCapacity)重置新的数组hash种子返回是否需要rehash
    transfer(newTable, initHashSeedAsNeeded(newCapacity));

    //替换旧数组
    table = newTable;

    //重新计算扩容门槛
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;

    //遍历旧数组用于转移到新数组
    for (Entry<K,V> e : table) {
        //只有当前的e != null 才需要拷贝
        while(null != e) {
            //临时保存一下 next,这点也是出现回环链表 死循环的原因
            Entry<K,V> next = e.next;

            //是否重新计算hash
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }

            //计算在新数组中的位置
            int i = indexFor(e.hash, newCapacity);
            //其实还是头插法,如果当前位置已经有元素了,就把该元素作为e的next
            e.next = newTable[i];
            //把e放入该位置
            newTable[i] = e;
            //把扩容前的e的next复制给e继续执行该流程,完成旧数组链表的下一个元素的转移
            e = next;
        }
    }
}

缺点

HashMap是线程不安全的,不安全的具体原因就是在高并发场景下,扩容可能产生死锁 (Jdk1.7存在)以及get操作可能带来的数据丢失

扩容死锁分析

死锁问题核心在于下面代码,多线程扩容导致形成的链表环! 以下代码是去除了与分析死锁无关的代码

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : table) {
        while(null != e) {
            //记录oldhash表中e.next 
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            //rehash计算出数组的位置(hash表中桶的位置) 
            int i = indexFor(e.hash, newCapacity);
            //e要插入链表的头部, 所以要先将e.next指向new hash表中的第一个元素
            e.next = newTable[i];
            //将e放入到new hash表的头部
            newTable[i] = e;
            //转移e到下一个节点, 继续循环下去
            e = next;
        }
    }
}

单线程扩容

**假设:**hash算法就是简单的key与length(数组长度)求余。hash表长度为2,如果不扩容, 那么元素key为3,5,7按照计算(key%table.length)的话都应该碰撞到table[1]上
**扩容:**hash表长度会扩容为4重新hash,key=3 会落到table[3]上(3%4=3), 当前e.next为key(7), 继续while循环重新hash,key=7 会落到table[3]上(7%4=3), 产生碰撞,这里采用的是头插入法,所以key=7的Entry会排在key=3前面(这里可以具体看while语句中代码)当前e.next为key(5), 继续while循环重新hash,key=5 会落到table[1]上(5%4=3), 当前e.next为null, 跳出while循环,resize结束
过程如下图所示:

在这里插入图片描述

多线程扩容

下面就是多线程同时put的情况了, 然后同时进入transfer方法中:假设这里有两个线程同时执行了put()操作,并进入了transfer()环节

while(null != e) {
    //线程1执行到此被调度挂起(cpu时间片轮转等发生上下文切换)
    //记录oldhash表中e.next
    Entry<K,V> next = e.next;
    //rehash计算出数组的位置(hash表中桶的位置) 
    int i = indexFor(e.hash, newCapacity);
     //e要插入链表的头部, 所以要先将e.next指向new hash表中的第一个元素
    e.next = newTable[i];
    //将e放入到new hash表的头部
    newTable[i] = e;
    //转移e到下一个节点, 继续循环下去
    e = next;
}

那么此时的状态为:

在这里插入图片描述


从上面的图我们可以看到,因为线程1的 e 指向了 key(3),而 next 指向了 key(7),在线程2 rehash 后,就指向了线程2 rehash 后的链表
然后线程1被唤醒了:

  1. 执行e.next = newTable[i],于是 key(3)的 next 指向了线程1的新 Hash 表,因为新 Hash 表为空,所以e.next = null
  2. 执行newTable[i] = e,所以线程1的新 Hash 表第一个元素指向了线程2新 Hash表的 key(3)。好了,e 处理完毕
  3. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

然后该执行 key(3)的 next 节点 key(7)了:

  1. 现在的 e 节点是 key(7),首先执行Entry next = e.next,那么 next 就是key(3)了
  2. 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7)
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(3)

此时的状态为:

在这里插入图片描述


然后又该执行 key(7)的 next 节点 key(3)了:

  1. 现在的 e 节点是 key(3),首先执行Entry next = e.next,那么 next 就是null
  2. 执行e.next = newTable[i],于是key(3) 的 next 就成了 key(7)
  3. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(3)
  4. 执行e = next,将 e 指向 next,所以新的 e 是 key(7)

这时候的状态如图所示:

在这里插入图片描述


很明显,环形链表出现了

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

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

相关文章

深度学习环境搭建

深度学习环境搭建0.引言1.Jupyter Notebook1.1.ANACONDA安装1.2.基于conda安装Jupyter Notebook1.3.Jupyter Notebook使用2.Pycharm2.1.安装Pycharm2.2.Pycharm设置0.引言 一步步采坑更新。 1.Jupyter Notebook 官方文档 安装参考 通过安装Anaconda来解决Jupyter Notebook…

300行HTML+CSS+JS代码实现动态圣诞树

文章目录1. 前言2. 效果展示3. 准备&#x1f351; 下载编译器&#x1f351; 下载插件4. 源码&#x1f351; HTML&#x1f351; JS&#x1f351; CSS5. 结语1. 前言 一年一度的圣诞节和考研即将来临&#xff0c;那么这篇文章将用到前端三大剑客 HTML CSS JS 来实现动态圣诞树…

Java面试题(六)多线程经典编程题

经典的多线程编程题猜数字游戏键盘输入练习3个线程轮流打印ABC多窗口买票猜数字游戏 题目说明&#xff1a;有2个线程&#xff0c;第一个线程A随机一个100内的数&#xff0c;第2个线程B来猜&#xff0c;B每次猜一个数后A会判断&#xff0c;如果猜对了程序结束&#xff0c;如果猜…

工具在接口测试中发挥什么样的作用?

接口测试究竟是什么&#xff1f;为什么要用接口测试&#xff1f;它有哪些工具呢&#xff1f;这一连串的问题敲击着我们&#xff0c;请带着这些问题&#xff0c;在本文中寻找答案&#xff0c;我将为您打开接口测试的大门。 1 初探接口测试 接口测试是什么。它检查数据的交换&…

从《我要投资》,看藏在“对立面”里的创业正解

文|智能相对论 作者|青月 六位07届的快乐男声选手在象山体验养鹅&#xff0c;意外出圈&#xff1b;随后播出的09届超女的怀旧综艺却热度一般&#xff0c;可见只有单纯的情怀消费并不能保证节目的口碑&#xff0c;只有建立在准确的节目定位与恰当的价值输出基础上&#xff0c;…

面试必备:从常见的存储引擎到混淆的锁分类,请上车

今天我们来总结一下MyISAM和InnoDB引擎下锁的种类及使用方法。 MySQL的四大常见存储引擎 谈到MyISAM和InnoDB了我们先来了解一下什么是存储引擎吧。MySQL中的数据用各种不同的技术存储在文件&#xff08;或者内存&#xff09;中&#xff0c;这些技术中的每一种技术都使用不同…

m基于贝叶斯理论的超分辨率重构算法matlab仿真,对比Tikhonov重构算法

目录 1.算法描述 2.仿真效果预览 3.MATLAB核心程序 4.完整MATLAB 1.算法描述 超分辨率(Super-Resolution)通过硬件或软件的方法提高原有图像的分辨率&#xff0c;通过一系列低分辨率的图像来得到一幅高分辨率的图像过程就是超分辨率重建。超分辨率成像&#xff08;SR-imagi…

一个内核oops问题的分析及解决

最近在调试设备时&#xff0c;遇到了一个偶发的开机死机问题。通过查看输出日志&#xff0c;发现内核报告了oops错误&#xff0c;如下所示&#xff08;中间省略了部分日志&#xff0c;以......代替&#xff09;&#xff1a; Unable to handle kernel NULL pointer dereference…

YOLOV7学习记录之训练过程

在前面学习YOLOV7的过程中&#xff0c;我们已经学习了其网络结构&#xff0c;然而实际上YOLOV7项目的难点并不在于其网络模型而是在于其损失函数的设计&#xff0c;即如何才能训练出来合适的bbox。 神经网络模型都有训练和测试&#xff08;推理&#xff09;过程&#xff0c;在Y…

QT JS交互、调用JS、传值

本文详细的介绍了QT JS交互、调用JS、传值的各种操作&#xff0c;包括QT向JS传递String字符串、包括QT向JS传递Int数字、包括QT向JS传递List数组&#xff0c;同时也接收JS向QT返回的List数组、JS向QT返回的Json、JS向QT返回的数字、JS向QT返回的字符串。 本文作者原创&#xff…

Vue基础8之Vue组件化编程、非单文件组件与单文件组件

Vue基础8Vue组件化编程对组件的理解一些概念的理解非单文件组件基本使用几个注意点组件的嵌套VueComponent一个重要的内置关系先导篇&#xff1a;原型对象正文&#xff08;可以理解为类的继承&#xff09;单文件组件Vue组件化编程 对组件的理解 传统方式&#xff1a; 使用组…

计算机网络-交换方式

目录电路交换&#xff08;Circuit Switching&#xff09;分组交换&#xff08;Packet Switching&#xff09;报文交换&#xff08;Message Switching&#xff09;电路交换、报文交换、分组交换的对比电路交换&#xff08;Circuit Switching&#xff09; 在电话问世后不久&#…

扫雷游戏的设计(百分百还原电脑操作)

目录 &#x1f332;了解扫雷游戏的作用原理并梳理思路 &#x1f332;扫雷游戏前期部分完善 &#x1f337;文件的创建 &#x1f337;创建菜单&#xff0c;完善主函数 &#x1f333;代码呈现&#xff1a; &#x1f332;扫雷游戏主题内容 &#x1f334;第一步初始化棋盘 &#x1…

Gradle中如何修改Springboot引入的依赖版本

扫描漏洞升级 不知道各位是否遇到过以下问题&#xff1a; 当下层项目将spring引入的某个依赖版本升级之后&#xff0c;上层项目只要指定了Springboot版本&#xff0c;那么还是会将这个版本改回去&#xff1f; 比如&#xff1a;现在有两个Springboot项目A、B&#xff0c;B项目…

Git安装和配置

GitGitee 官网安装配置教程&#xff1a;https://gitee.com/help/articles/4104本文是以官网教程为基础而展开的实践笔记。初学者可以以本文为引入&#xff0c;但建议最终以官方文档为最终深入学习的参考。一、 下载和安装Git 1、官网下载&#xff1a;https://git-scm.com 如果对…

HTML5基础

HTML5 文章目录HTML5概述开发工具浏览器开发软件DemoHTML5语法HTML5标签HTML5标签属性HTML5文档注释HTML5文档结构头部内容主体内容DemoHTML5常见标签常见块级标签标题标签水平线标签段落标签换行标签引用标签预格式标签无序列表标签有序列表标签定义列表标签分区标签常见行级标…

【Java寒假打卡】Java基础-继承

【Java寒假打卡】Java基础-继承一、继承的好处和弊端二、继承的成员变量访问特点三、重写方法四、方法重写的注意事项五、权限修饰符六、构造方法一、继承的好处和弊端 继承的好处 提高了代码的复用性 提高了代码的维护性 让类和类之间产生了关系 是多态的前提 继承的弊端 …

Flink-使用filter和SideOutPut进行分流操作

文章目录1.什么是分流&#xff1f;2. 过滤器(filter)3. 使用侧输出流&#xff08;SideOutput&#xff09;&#x1f48e;&#x1f48e;&#x1f48e;&#x1f48e;&#x1f48e; 更多资源链接&#xff0c;欢迎访问作者gitee仓库&#xff1a;https://gitee.com/fanggaolei/learni…

四、网络层(七)网络层设备

目录 7.1 路由器的组成和功能 7.2 路由表与路由转发 7.1 路由器的组成和功能 路由器是一种具有多个输入/输出端口的专用计算机&#xff0c;其任务是连接不同的网络&#xff08;可以是异构的&#xff09;并完成路由转发。在多个逻辑网络&#xff08;即多个广播域&#xff…

Vulnhub靶机:HACKADEMIC_ RTB1

目录介绍信息收集主机发现主机信息探测网站探测Sql注入挂马提权介绍 系列&#xff1a;Hackademic&#xff08;此系列共2台&#xff09; 发布日期&#xff1a;2011年9月6日 难度&#xff1a;初级 运行环境&#xff1a;VMware Workstation 目标&#xff1a;取得 root 权限 flag…