Java HashMap源码浅析

简介: 大体上就是新建一个newTab,是原来table大小的两倍,然后把原有数据做放到newTab中。先不管Tree是怎么裂变的(还没搞懂),我们来看下单链表怎么裂变的。 这里直接把一个单链表裂变成了两个。在odltab中第j位置的单链表,裂变成两条单链表,一条放newtab中j位置,另一条放newtab中i+oldcap的位置,每个j都是这么做的,就是这么神奇, 很长时间我一直没理解这么简单处理,如何保证在同一条链表中的节点都有相同的hash值?

之前虽然很频繁使用java的hashmap,但一直都是纯用,至于里面怎么实现的,一直都是糊里糊涂的。今年4月份跳槽找工作,大概看了一下HashMap的源码,在面试过程中也被多位面试官问到HashMap的相关问题,有些问题也没回答出来。本来几个月前就想着写一篇相关源码解析的博客(主要是加深自己的理解),一直拖到现在,接下来就跟我一起看下HashMap的实现(基于jdk8)。

 9a6e3f832a0a384117f67e4f2a4b1125_70.png

 HashMap实现了Map,Cloneable,Serializable三个接口,Map定义了必要方法,Cloneable意味着HashMap是可被clone的,Serializable以为这HashMap可以被序列化。但是AbstractMap是什么鬼?打开AbstractMap的源码就会发现注释第一句就告诉你,AbstractMap给Map提供了一个骨架,事实上它实现了Map的几乎所有的方法,除了entrySet()。AbstractMap既然实现了Map接口,那为什么HashMap还要实现Map接口? 好吧关于这个问题,有人说是个作者的一个失误,见StackOverflow,我觉得这个说法站不住脚啊,如果是个失误,为啥到jdk10还没被修复,跳过这个问题。

 打开HashMap的源码,跳过一堆注释,最先看到的是几个常量,这几个常量是缺省时的默认值,非常重要,它们决定这HashMap在使用过程中内部的发生的一些行为,我们挨个看一下。


long serialVersionUID = 362498820763181265L;   //用来在序列化时校验用的  
int DEFAULT_INITIAL_CAPACITY = 1 << 4;   //缺省时HashMap的初始容量 16
int MAXIMUM_CAPACITY = 1 << 30;  //最大容量  
float DEFAULT_LOAD_FACTOR = 0.75f;  //缺省时的默认负载因子,当map中的存储量占总容量的75%以上,hashmap就会把自身容量扩张一倍
int TREEIFY_THRESHOLD = 8;  // 触发树化阀值
int MIN_TREEIFY_CAPACITY = 64;  // 真正树化阀值 

了解一个类,应该最先从其构造函数开始,HashMap可选的构造参数并不多,最核心的就这一个构造方法,如下。


 

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

这个构造函数做的事很简单,只是初始化了两个值,一个是负载因子,如果用户用自定义切校验通过就会抛弃默认的0.75负载因子。另一个是threshold,loadFactor是个负载因子的比例,threhold才是实际值,HashMap会根据threhold具体值来做resize()。这里我们顺带看下tableSizeFor()的实现。


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

这个函数就就是找到最小那个比cap大的2^n。

 如上所示,其实构造函数什么都没做。内部空间初始化也没做,那什么时候做呢?其实HashMap存储初始化的动作都是插入值的时候做的,这样设计有个好处,可以避免你虽然new了一个HashMap,但写入数据导致的资源浪费。

  接下来我们看下HashMap内部是怎么存储数据的。HashMap中有个内部静态类Node<K,V>,这个用来存储数据的实体,看下代码。


static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        /**
        * 省略一些简单方法
        */
    }

这个类里除了用户可见的K,V之外,还额外存储了hash值,这个hash值就是K的hash值,单独存下是为了优化,不需要每次用到的时候都计算K的hash值。next这个就是单链表的next指针了,HashMap解决hash值冲突的方式就是是开一条单链表,Jdk8之后就不仅仅是单链表了,后续我们会详解介绍。

 HashMap中有个Node<K,V>[] table; 这个数组就是所有数据存储的地方了。它会在第一次插入数据时发现table为null候才被初始化。


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            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
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        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;
    }

既然贴出来了这段代码了,我们就先来看下这段,再看HashMap的resize()。最开始就检查table,如果table是null,就用resize做初始化,然后在安插新节点。如果遇到hash值碰撞,如果发现是同一个key,就覆盖。否在有两种情况,要么单链表插入,要么红黑树插入,一个HashMap的内部存储可能长这个样子,树化后的某些单链表可能会变成一颗树(示意图,并不代表真实情况)。

24f441090d90c47424c5d77aacf80cd2_70.png

 这里就遇到jdk对于hashmap指针碰撞的优化了,如果过多的K,V都有同样的hash值,就会变成一条非常长的链,增删查的性能就非常低了。所以jdk8在链表长度达到8的时候就会触发链表的树话,把单链表变成一个红黑树,可以把各种操作的时间复杂度从O(n)降低到O(logn)。

 事实上,链表长度到8,虽然会触发树化,但并不一定会树化,而是会优先选择resize(),只有链表长度达到64才会树化,其实我并不理解为什么要再设个64的阀值才会树化,可能是在节点树少的时候,树的平局操作时间复杂度不会比单链表的优太多吧。

 我觉得在HashMap中resize()是HashMap的核心,也是很多问题的根源。虽然resize()对用户不可见,但它是保证HashMap性能最重要的一般,当然它也是HashMap并发问题的罪魁祸首。

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        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)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        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;
    }

大体上就是新建一个newTab,是原来table大小的两倍,然后把原有数据做放到newTab中。先不管Tree是怎么裂变的(还没搞懂),我们来看下单链表怎么裂变的。 这里直接把一个单链表裂变成了两个。在odltab中第j位置的单链表,裂变成两条单链表,一条放newtab中j位置,另一条放newtab中i+oldcap的位置,每个j都是这么做的,就是这么神奇, 很长时间我一直没理解这么简单处理,如何保证在同一条链表中的节点都有相同的hash值?

 其实也很简单,就是按hash值第oldCap那位0和1做了区分。因为我们cap每次都是以2为系数增长的,然后按cap-1为掩码来计算位置的,当容量扩大一倍后,只会有一部分最高位是1的才需要移动。比如hash值最后4位都是是0101的节点,其在newtab中新的位置要么是10101,要么是00101。

 今天最后一个问题,为什么HashMap不是线程安全的? 罪魁祸首还是resize(),如果在多线程插入的情况下,多个线程有可能同时执行resize(),多线程操作一个单链表,很大几率会出现数据一致性的问题,甚至有可能会在链表中出现环。

目录
相关文章
|
13天前
|
Java
Java之HashMap详解
本文介绍了Java中HashMap的源码实现(基于JDK 1.8)。HashMap是基于哈希表的Map接口实现,允许空值和空键,不同步且线程不安全。文章详细解析了HashMap的数据结构、主要方法(如初始化、put、get、resize等)的实现,以及树化和反树化的机制。此外,还对比了JDK 7和JDK 8中HashMap的主要差异,并提供了使用HashMap时的一些注意事项。
Java之HashMap详解
|
8天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
31 2
|
28天前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
42 1
|
30天前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
57 2
|
2天前
|
数据采集 人工智能 Java
Java产科专科电子病历系统源码
产科专科电子病历系统,全结构化设计,实现产科专科电子病历与院内HIS、LIS、PACS信息系统、区域妇幼信息平台的三级互联互通,系统由门诊系统、住院系统、数据统计模块三部分组成,它管理了孕妇从怀孕开始到生产结束42天一系列医院保健服务信息。
13 4
|
13天前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
30 3
|
18天前
|
运维 自然语言处理 供应链
Java云HIS医院管理系统源码 病案管理、医保业务、门诊、住院、电子病历编辑器
通过门诊的申请,或者直接住院登记,通过”护士工作站“分配患者,完成后,进入医生患者列表,医生对应开具”长期医嘱“和”临时医嘱“,并在电子病历中,记录病情。病人出院时,停止长期医嘱,开具出院医嘱。进入出院审核,审核医嘱与住院通过后,病人结清缴费,完成出院。
55 3
|
23天前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
28天前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
54 5
|
26天前
|
移动开发 前端开发 JavaScript
java家政系统成品源码的关键特点和技术应用
家政系统成品源码是已开发完成的家政服务管理软件,支持用户注册、登录、管理个人资料,家政人员信息管理,服务项目分类,订单与预约管理,支付集成,评价与反馈,地图定位等功能。适用于各种规模的家政服务公司,采用uniapp、SpringBoot、MySQL等技术栈,确保高效管理和优质用户体验。