您当前的位置:首页 > 电脑百科 > 程序开发 > 编程百科

HashMap核心原理分析

时间:2022-09-13 11:57:56  来源:今日头条  作者:eclipse2019

学习目标

1、hash冲突的解决办法有哪几种

2、HashTable、hashmap、CHM三者之间的区别

3、HashMap的默认长度是多少?默认扩容因子是多少?

4、HashMap它是怎么解决hash冲突的

5、HashMap为什么扩容是2的幂次方

6、谈一下HashMap中put是如何实现的?

7、谈一下hashMap中什么时候需要进行扩容,扩容resize()又是如何实现的?

8、谈一下hashMap中get是如何实现的?

9、为什么是16?为什么必须是2的幂?如果输入值不是2的幂比如10会怎么样?

10、HashMap和HashTable的区别

11、hashmap中的扰动函数有什么作用

12、hashmap的底层数据结构(1.7和1.8版本)

13、hashmap为什么是线程不安全的(1.7和1.8版本)

14、在什么情况下hashmap的链表会转化成红黑树

15、HashMap的key可以为null吗?CHM呢?

第1章 前置知识

1、Hash表​​​​​​​​​​​​​

Hash函数:MD5、SHA

Hash表:通过hash函数来计算数据位置的数据结构

HashMap通过数组去存储

2、Hash冲突

多个不同的key通过hash函数运算之后落在同一个数组下标的位置。

解决方案:

  • ThreadLocal解决方案:线性探索(开发寻址法)
  • HashMap解决方案:链式地址法
  • 再Hash法(通过多个hash函数)

3、锁粒度

  • hashtable:是对整个数组加锁,两个线程对同一个hashtable进行操作时,只可能有一个线程进行put。
  • 1.7CHM:对数组中的每个segment加锁,不同线程对同一个CHM进行操作时,如果hash计算没有落在同一个segment时,就可以同时操作。每个segment里面存的也是一个16个长度的数组,然后数组里面存链表
  • 18CHM:对数组中的每个node加锁,不同线程对同一个CHM进行操作时,如果hash计算没有落在同一个node时,就可以同时操作。每个node里面存的是存链表或红黑树。

第2章 HashMap

2.1 从new说起

HashMap hashMap=new HashMap(5);
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);  //代表hashMap的容量大于这个的时候会进行resize操作
}

不管传多大的容量,容量都是最靠近设置值的2的幂次方,且大于传入值

2.2 右移左移补充

<<表示左移移,不分正负数,低位补0;

注:以下数据类型默认为byte-8位

左移时不管正负,低位补0

正数:r = 20 << 2

  20的二进制补码:0001 0100

  向左移动两位后:0101 0000

       结果:r = 80

r = 20 << 3

  20的二进制补码:0001 0100

  向左移动两位后:1010 0000

原码:1110 0000

       结果:r = -96

负数:r = -20 << 2

  -20 的二进制原码 :1001 0100

  -20 的二进制反码 :1110 1011

  -20 的二进制补码 :1110 1100

  左移两位后的补码:1011 0000

        反码:1100 1111

        原码:1101 0000

        结果:r = -80

>>表示右移,如果该数为正,则高位补0,若为负数,则高位补1;

注:以下数据类型默认为byte-8位

正数:r = 20 >> 2

  20的二进制补码:0001 0100

  向右移动两位后:0000 0101

       结果:r = 5

负数:r = -20 >> 2

  -20 的二进制原码 :1001 0100

  -20 的二进制反码 :1110 1011

  -20 的二进制补码 :1110 1100

  右移两位后的补码:1111 1011

        反码:1000 0100

        原码:1000 0101

        结果:r = -5

>>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0

正数: r = 20 >>> 2

    的结果与 r = 20 >> 2 相同;

负数: r = -20 >>> 2

注:以下数据类型默认为int 32位

  -20:源码:10000000 00000000 00000000 00010100

    反码:11111111 11111111 11111111 11101011

    补码:11111111 11111111 11111111 11101100

    右移:00111111 11111111 11111111 11111011

    结果:r = 1073741819

-1源码: 10000000 00000000 00000000 00000001

反码: 11111111 11111111 11111111 11111110

补码: 11111111 11111111 11111111 11111111

01111111 11111111 11111111 11111111

11111111 11111111 11111111 11111111

//保证
static final int tableSizeFor(int cap) {
    int n = cap - 1;  //减1.防止传入的刚好是2的幂次方的时候,比如传入4,不减1,会得到7;
    n |= n >>> 1;  //右移一位,然后|运算,能保证高2位是1,是4                                                         n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;   //类推到最大的2的32-1,也就是32个1!!因为JAVA中int4字节,也就是32位,达到int的最大值
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;  //再加1,
}

举例 传入的cap是14

n=cap-1 ;  13   二进制位0000 1101
    
n |= n >>> 1;    n>>>1 为 0000 0110   与n|运算    得到00001111
n |= n >>> 2;    n>>>2 为 0000 0011   与n|运算    得到00001111
n |= n >>> 4;    n>>>4 为 0000 0000   与n|运算    得到00001111
最终结果就是 00010000

2.3 扰动函数

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

右移16位与本来的异或;能使高16位与低16位都参与hash运算!!减少hash冲突

比如

1110 1000  >>>4   0000 1110
0000 1110    
---------------
1110 0110    

2.4 继续put方法

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) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //判断table是否为空
    if ((tab = table) == null || (n = tab.length) == 0)
        //初始化
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)   //不存在hash冲突
        tab[i] = newNode(hash, key, value, null);
    else {    //hash冲突
        Node<K,V> e; K k;
        if (p.hash == hash &&  //hash值相等
            ((k = p.key) == key || (key != null && key.equals(k))))  //key也相等
            e = p;  //直接将已有的给到e
        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); //添加到链表尾部
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st   //当链表长度达到8,转变为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))   //hash跟key都一样,链表里面覆盖
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mApping for 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;
}

2.5 扩容(初始化)

final Node<K,V>[] resize() {   
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;  //得到原来table的长度.假如是4
    int oldThr = threshold;  //得到原来的扩容临界值   4*0.75=3
    int newCap, newThr = 0;  //新的table的长度与扩容临界值 
    if (oldCap > 0) {  //如果table有值,代表不是初始化   
        if (oldCap >= MAXIMUM_CAPACITY) {   //判断原来的table长度是不是到了最大值,如果到了最大值,不再扩容,直接返回原来的数组
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&   //newCap为oldCap的2倍,并且原来的长度>=16,临界值为原有临界值的2倍,这时newCap=8
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold,结果是跟newCap*0.75一样的,必须大于等于16,不然4的扩容是3,*2就会有问题
    }
    else if (oldThr > 0) // initial capacity was placed in threshold  //当没有数据的时候,初始化容量就为阈(yu)值
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults   //默认值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {  //当原来table的长度小于16的时候
        float ft = (float)newCap * loadFactor;   //newCap为原来长度的2倍*0.75     也就是8*0.75=6 
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);    //新的扩容临界值为6
    }
    threshold = newThr;  //扩容值变成了6
    @SuppressWarnings({"rawtypes","unchecked"})   //不用再编译完成后出现警告信息,忽略"rawtypes","unchecked"
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  // 新的数组长度为8
    table = newTab;     //table赋值为newTab
    if (oldTab != null) {  //进行数据迁移
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;   //便于GC
                if (e.next == null)  //没有next,代表不是链表,就是数组
                    newTab[e.hash & (newCap - 1)] = e;   //hash取模放到数组位置
                else if (e instanceof TreeNode)  //如果是红黑树,按红黑树转移
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order   //链表,链表转移
                    //低位链表,也就是当hash取模小于old的时候
                    Node<K,V> loHead = null, loTAIl = null;
                    //高位链表,也就是当hash取模大于等于old的时候
                    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;
                    }
                    //如果超过了原来old,放在old的长度+j
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

2.6 链表转红黑树

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)   //数组不到64,也不会转红黑树
        resize();  //扩容
    else if ((e = tab[index = (n - 1) & hash]) != null) {  
        TreeNode<K,V> hd = null, tl = null;   // 定义首、尾节点
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);  //将该节点转换为 树节点
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);  // 继续遍历链表
        
          // 到目前为止 也只是把Node对象转换成了TreeNode对象,把单向链表转换成了双向链表
        
        if ((tab[index] = hd) != null)
            hd.treeify(tab);  //转为红黑树
    }
}

HashMap底层为什么线程不安全,作者用了PPT演示了过程,文章中贴不出来,如果感兴趣的同学可以私聊作者发给你



Tags:HashMap   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Search: HashMap  点击:(11)  评论:(0)  加入收藏
HashMap:Java中的高效数据结构
HashMap是Java中常用的数据结构之一,它实现了Map接口,并且提供了快速的查找、插入和删除操作。HashMap的底层数据结构是数组和链表(或红黑树)的组合,这种数据结构被称为哈希表(Has...【详细内容】
2023-11-24  Search: HashMap  点击:(329)  评论:(0)  加入收藏
HashMap的底层数据结构
在 JDK1.8 中,HashMap 还引入了一个新的概念,叫做负载因子(load factor),它是指哈希表中键值对的数量与数组长度的比值。当键值对的数量超过了负载因子与数组长度的乘积时,就会...【详细内容】
2023-09-15  Search: HashMap  点击:(239)  评论:(0)  加入收藏
HashMap 的基础结构,必须掌握!
HashMap 是一种散列表,它存储的内容是键值对(key-value)映射。在 HashMap 中,每个键(key)映射到一个值(value)。散列表的工作原理是:当通过 put() 方法将键值对存储在 HashMap...【详细内容】
2023-09-14  Search: HashMap  点击:(277)  评论:(0)  加入收藏
HashMap 是怎么解决哈希冲突的?
前言 今天来分享一道比较好的面试题,“HashMap 是怎么解决哈希冲突的?”对于这个问题,我们一起看看考察点和比较好的回答吧!考察点 现在的企业级开发中HashMap几乎是...【详细内容】
2023-09-11  Search: HashMap  点击:(198)  评论:(0)  加入收藏
搞懂hashMap底层原理
说明hashMap在java1.7和java1.8版本中有做一些调整,我们本篇只说java1.7的hashMap。数据结构hashMap的数据结构是由数组和链表组成,table是一个存放Entry对象的数组,每个Entry...【详细内容】
2023-08-03  Search: HashMap  点击:(105)  评论:(0)  加入收藏
HashMap线程不安全体现在哪里?
HashMap线程不安全体现在哪里?如果你到现在还不清楚赶紧看下去,明明白白补一补~。在Java中,HashMap是一种常用的数据结构,它以键值对的形式存储和管理数据。然而,由于HashMap在...【详细内容】
2023-04-27  Search: HashMap  点击:(291)  评论:(0)  加入收藏
如何实现线程安全的HashMap?
要实现线程安全的 HashMap,可以考虑以下几种方法: 使用 ConcurrentHashMap:ConcurrentHashMap 是线程安全的 HashMap 实现,采用了分段锁的机制,可以提高并发性能。 使用 Collecti...【详细内容】
2023-03-21  Search: HashMap  点击:(266)  评论:(0)  加入收藏
三分钟轻松搞懂 HashMap 死循环问题!
HashMap 死循环发生在 JDK 1.7 版本中,形成死循环的原因是 HashMap 在 JDK 1.7 使用的是头插法,头插法 + 链表 + 多线程并发 + HashMap 扩容,这几个点加在一起就形成了 HashMap...【详细内容】
2023-01-31  Search: HashMap  点击:(256)  评论:(0)  加入收藏
HashMap核心原理分析
学习目标1、hash冲突的解决办法有哪几种2、HashTable、hashmap、CHM三者之间的区别3、HashMap的默认长度是多少?默认扩容因子是多少?4、HashMap它是怎么解决hash冲突的5、Hash...【详细内容】
2022-09-13  Search: HashMap  点击:(134)  评论:(0)  加入收藏
▌简易百科推荐
即将过时的 5 种软件开发技能!
作者 | Eran Yahav编译 | 言征出品 | 51CTO技术栈(微信号:blog51cto) 时至今日,AI编码工具已经进化到足够强大了吗?这未必好回答,但从2023 年 Stack Overflow 上的调查数据来看,44%...【详细内容】
2024-04-03    51CTO  Tags:软件开发   点击:(5)  评论:(0)  加入收藏
跳转链接代码怎么写?
在网页开发中,跳转链接是一项常见的功能。然而,对于非技术人员来说,编写跳转链接代码可能会显得有些困难。不用担心!我们可以借助外链平台来简化操作,即使没有编程经验,也能轻松实...【详细内容】
2024-03-27  蓝色天纪    Tags:跳转链接   点击:(12)  评论:(0)  加入收藏
中台亡了,问题到底出在哪里?
曾几何时,中台一度被当做“变革灵药”,嫁接在“前台作战单元”和“后台资源部门”之间,实现企业各业务线的“打通”和全域业务能力集成,提高开发和服务效率。但在中台如火如荼之...【详细内容】
2024-03-27  dbaplus社群    Tags:中台   点击:(8)  评论:(0)  加入收藏
员工写了个比删库更可怕的Bug!
想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看。可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!给大家分享一下(不是公开处刑),希望朋友们...【详细内容】
2024-03-26  dbaplus社群    Tags:Bug   点击:(5)  评论:(0)  加入收藏
我们一起聊聊什么是正向代理和反向代理
从字面意思上看,代理就是代替处理的意思,一个对象有能力代替另一个对象处理某一件事。代理,这个词在我们的日常生活中也不陌生,比如在购物、旅游等场景中,我们经常会委托别人代替...【详细内容】
2024-03-26  萤火架构  微信公众号  Tags:正向代理   点击:(10)  评论:(0)  加入收藏
看一遍就理解:IO模型详解
前言大家好,我是程序员田螺。今天我们一起来学习IO模型。在本文开始前呢,先问问大家几个问题哈~什么是IO呢?什么是阻塞非阻塞IO?什么是同步异步IO?什么是IO多路复用?select/epoll...【详细内容】
2024-03-26  捡田螺的小男孩  微信公众号  Tags:IO模型   点击:(8)  评论:(0)  加入收藏
为什么都说 HashMap 是线程不安全的?
做Java开发的人,应该都用过 HashMap 这种集合。今天就和大家来聊聊,为什么 HashMap 是线程不安全的。1.HashMap 数据结构简单来说,HashMap 基于哈希表实现。它使用键的哈希码来...【详细内容】
2024-03-22  Java技术指北  微信公众号  Tags:HashMap   点击:(11)  评论:(0)  加入收藏
如何从头开始编写LoRA代码,这有一份教程
选自 lightning.ai作者:Sebastian Raschka机器之心编译编辑:陈萍作者表示:在各种有效的 LLM 微调方法中,LoRA 仍然是他的首选。LoRA(Low-Rank Adaptation)作为一种用于微调 LLM(大...【详细内容】
2024-03-21  机器之心Pro    Tags:LoRA   点击:(12)  评论:(0)  加入收藏
这样搭建日志中心,传统的ELK就扔了吧!
最近客户有个新需求,就是想查看网站的访问情况。由于网站没有做google的统计和百度的统计,所以访问情况,只能通过日志查看,通过脚本的形式给客户导出也不太实际,给客户写个简单的...【详细内容】
2024-03-20  dbaplus社群    Tags:日志   点击:(4)  评论:(0)  加入收藏
Kubernetes 究竟有没有 LTS?
从一个有趣的问题引出很多人都在关注的 Kubernetes LTS 的问题。有趣的问题2019 年,一个名为 apiserver LoopbackClient Server cert expired after 1 year[1] 的 issue 中提...【详细内容】
2024-03-15  云原生散修  微信公众号  Tags:Kubernetes   点击:(6)  评论:(0)  加入收藏
站内最新
站内热门
站内头条