本文将从以下方面结合源码进行分析:自动扩容、初始化与懒加载、哈希计算、位运算(默认采用JDK1.8)。
 

自动扩容

扩容操作发生在putVal最后部分,在增加元素后才判断是否需要扩容,如果超过阈值,会自动扩容。
 
 
 
 
 
 
 
 
 
 
这里扩容都是<<1翻倍进行扩容的。
扩容时节点数组进行数据转移的三种情况:
  • 节点的元素无后继节点:
  直接根据节点hash值重新计算下标,然后复制到新的数组中。
  • 节点为树节点:
  进行红黑树的扩容操作。
  因为capacity变化后,hash&(cap-1)可能得到不同结果。原有的红黑树变成高低位两个红黑树。低位红黑树下标位置和旧数组相同,高位红黑树下标位置在旧数组的基础上+oldCap,因为hash&(2*cap-1)结果等于hash&(cap-1)或者hash&(cap-1)+cap。
   红黑树扩容时遍历原有链表,然后根据新的hash值重新分为低位链表和高位链表。
  若所有元素都在低位链表或高位链表,则不需要重新树化,直接将链表头节点插入数组对应位置;
  若低位链表或高位链表的数量<7,则深拷贝低位或高位树节点链表得到普通节点新链表(低位或高位树节点链表含有树的偏序关系,拷贝得到的普通节点链表只有链表的偏序关系),并将新链表头节点插入数组对应位置。
  如果数量>=7则深拷贝低位或高位树节点链表得到普通节点新链表,再进行树化。
  具体源码分析如下:
 1 final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
2 // 获取自身树节点
3 TreeNode<K,V> b = this;
4 // Relink into lo and hi lists, preserving order
5 // 低位链表的头尾节点
6 TreeNode<K,V> loHead = null, loTail = null;
7 // 高位链表的头尾节点
8 TreeNode<K,V> hiHead = null, hiTail = null;
9 // 低位链表节点数量、高位链表节点数量
10 int lc = 0, hc = 0;
11 for (TreeNode<K,V> e = b, next; e != null; e = next) {
12 next = (TreeNode<K,V>)e.next;
13 // 这步操作不是多余的,在e为低位或高位链表最终尾节点时起到赋空作用
14 e.next = null;
15 // 如果仍然在原位置,则加入低位链表
16 if ((e.hash & bit) == 0) {
17 if ((e.prev = loTail) == null)
18 loHead = e;
19 else
20 loTail.next = e;
21 loTail = e;
22 ++lc;//低位链表数量+1
23 }
24 else {
25 // 如果是在新的位置(原索引值+oldcap),加入高位链表
26 if ((e.prev = hiTail) == null)
27 hiHead = e;
28 else
29 hiTail.next = e;
30 hiTail = e;
31 ++hc;// 高位链表数量+1
32 }
33 }
34 // 低位链表不为空
35 if (loHead != null) {
36 // 低位链表数量不超过6,则深拷贝低位树节点链表得到普通节点新链表,并将新链表头部放入数组
37 if (lc <= UNTREEIFY_THRESHOLD)
38 tab[index] = loHead.untreeify(map);
39 else {
40 tab[index] = loHead;
41 // 如果高位链表为空,说明全部元素都在低位链表中,因为原链表已经是树化的了,所以不用再转为红黑树
42 if (hiHead != null) // (else is already treeified)
43 loHead.treeify(tab);
44 }
45 }
46 // 高位链表不为空
47 if (hiHead != null) {
48 // 高位链表数量不超过6,则深拷贝树节点高位链表得到普通节点新链表,并将新链表头部放入数组
49 if (hc <= UNTREEIFY_THRESHOLD)
50 tab[index + bit] = hiHead.untreeify(map);
51 else {
52 tab[index + bit] = hiHead;
53 // 如果低位链表为空,说明全部元素都在高位链表中,因为原链表已经是树化的了,所以不用再转为红黑树
54 if (loHead != null)
55 hiHead.treeify(tab);
56 }
57 }
58 }
  • 节点为链表节点:
  进行链表的复制操作。操作和红黑树扩容操作非常相似。也是先遍历原有链表节点,然后根据新的hash值分为低位链表和高位链表。
  分完高低位链表后,将头节点插入数组对应位置即可。
  具体源码分析如下:
 1 // case3:节点为链表节点,进行链表的赋值操作
2 else { // preserve order
3 // 低位Node链表头节点和尾节点
4 Node<K,V> loHead = null, loTail = null;
5 // 高位Node链表头节点和尾节点
6 Node<K,V> hiHead = null, hiTail = null;
7 Node<K,V> next;
8 // 遍历原链表,拆分成低位链表和高位链表
9 do {
10 next = e.next;
11 // 如果是在原位置,则加入低位链表
12 if ((e.hash & oldCap) == 0) {
13 if (loTail == null)
14 loHead = e;
15 else
16 loTail.next = e;
17 loTail = e;
18 }
19 else {
20 // 如果不在原位置,加入高位链表
21 if (hiTail == null)
22 hiHead = e;
23 else
24 hiTail.next = e;
25 hiTail = e;
26 }
27 } while ((e = next) != null);
28 // 如果低位链表不为空
29 if (loTail != null) {
30 // 尾部节点赋空并将头部节点放入数组指定位置
31 loTail.next = null;
32 newTab[j] = loHead;
33 }
34 // 如果高位链表不为空
35 if (hiTail != null) {
36 // 尾部节点赋空并将头部节点放入数组指定位置
37 hiTail.next = null;
38 newTab[j + oldCap] = hiHead;
39 }
40 }

 在jdk1.8之前,hashmap在多线程环境中使用会出现死链问题。如果有多个线程同时进行扩容操作,一个线程拿到链表头节点和后继节点时挂起,另一个线程执行完扩容操作,会使得这两个节点互相依赖,出现死链,导致第一个线程不能退出循环,CPU使用率飙升。

jdk1.8将原来的头插法改为了尾插法,同时复制链表时不再是遍历一个节点就插入,而是使用高低位链表。待遍历完所有节点后,再将高低位链表放入新数组对应位置。

但是仍然不建议在多线程环境下使用,仍然会有数据缺失和数据重复等等问题。

初始化与懒加载

hashmap节点数组的定义和初始化不会在构造函数中完成,而是在首次执行put()操作时才完成的。
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
2 boolean evict) {
3 Node<K,V>[] tab; Node<K,V> p; int n, i;
4 // 如果节点数组未初始化或为空,则进行初始化操作
5 if ((tab = table) == null || (n = tab.length) == 0)
6 n = (tab = resize()).length;
resize()中会设置默认的初始化容量DEFAULT_INITIAL_CAPACITY为16,扩容的阈值为0.75*16 = 12,即哈希桶数组中元素达到12个便进行扩容操作。
最后创建容量为16的Node数组,并赋值给成员变量哈希桶table,即完成了HashMap的初始化操作。
 1 final Node<K,V>[] resize() {
2 // 获取原有table
3 Node<K,V>[] oldTab = table;
4 int oldCap = (oldTab == null) ? 0 : oldTab.length;
5 int oldThr = threshold;
6 // 新容量、新阈值
7 int newCap, newThr = 0;
8 ......................
9 else { // zero initial threshold signifies using defaults
10 // 原容量和阈值都<=0,则用默认值初始化,默认容量16,负载因子0.75,对应的是hashmap没带参数初始化。
11 newCap = DEFAULT_INITIAL_CAPACITY;
12 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
13 }

哈希计算

null的hash值为0。计算key的hash值时先获取key的32位hashCode,然后将hashcode&(hashcode>>>16),等效于高16位不变,高16位与低16位作异或,结果为新的低16位。将高16位与低16位进行异或的操作称之为扰动函数,目的是将高位的特征融入到低位之中,降低哈希冲突的概率。
1 static final int hash(Object key) {
2 int h;
3 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4 }
ConcurrentHashMap中经过扰乱函数处理之后,需要与HASH_BITS做与运算,HASH_BITS为0x7ffffff,即只有最高位为0,这样运算的结果使hashCode永远为正数。在ConcurrentHashMap中,预定义了几个特殊节点的hashCode,如:MOVED、TREEBIN、RESERVED,它们的hashCode均定义为负值。因此,将普通节点的hashCode限定为正数,也就是为了防止与这些特殊节点的hashCode产生冲突。
1 static final int MOVED     = -1; // hash for forwarding nodes
2 static final int TREEBIN = -2; // hash for roots of trees
3 static final int RESERVED = -3; // hash for transient reservations
4 static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
5 static final int spread(int h) {
6 return (h ^ (h >>> 16)) & HASH_BITS;
7 }

哈希冲突

如有多个key计算得到的hashCode相同,就会产生hash冲突。jdk1.8的hashmap使用了链表法和红黑树去处理hash冲突。
当出现hash冲突时,将新插入的节点通过尾插法插入到链表的尾部。当链表的长度超过8且数组的capacity>=64则将链表转为红黑树。
 1   // 如果链表的数量超过了8,且数组cap大小>=64则转为红黑树
2 // 如果链表数量超过了8,但数组cap大小<64则resize()扩容
3 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
4 treeifyBin(tab, hash);
5
6 final void treeifyBin(Node<K,V>[] tab, int hash) {
7 int n, index; Node<K,V> e;
8 // 如果数组长度没达到64就扩容
9 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
10 resize();

位运算

计算数组大小的方法,给定一个输出值,找到大于等于给定值的最小的2^n。
1 static final int tableSizeFor(int cap) {
2 int n = cap - 1;
3 n |= n >>> 1;
4 n |= n >>> 2;
5 n |= n >>> 4;
6 n |= n >>> 8;
7 n |= n >>> 16;
8 return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
9 }
这里是实现了找>=cap的最小2^n。cap为int类型,长度32位。
对于一个正数,找大于该数的最小的2^n,都可以采用这种方式,将n最高位后面全部置为1,然后加1,因为位运算非常快速,这种方法比找到最高位然后构造新的数要更快。
至于数组大小设置为2^n,是为了提高数组的空间利用率。计算索引的方法是hash&(cap-1),当cap为2^n,cap-1为00001111(忽略前置0)的形式,这样得到的索引位置为[0,cap-1],每一个位置都由机会。如果cap不为2^n,比如15,那么cap-1为00001110,计算得到的索引只有0,2,4,6,8,10,12,14这些位置,空间利用率只有50%。

HashMap底层原理分析的更多相关文章

  1. HashMap底层原理分析(put、get方法)

    1.HashMap底层原理分析(put.get方法) HashMap底层是通过数组加链表的结构来实现的.HashMap通过计算key的hashCode来计算hash值,只要hashCode一样,那ha ...

  2. 基础进阶(一)之HashMap实现原理分析

    HashMap实现原理分析 1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端. 数组 数组存储区间是连续的,占用内存严重,故空间复杂的很大.但数组的二 ...

  3. 最简单的HashMap底层原理介绍

    HashMap 底层原理  1.HashMap底层概述 2.JDK1.7实现方式 3.JDK1.8实现方式 4.关键名词 5.相关问题 1.HashMap底层概述 在JDK1.7中HashMap采用的 ...

  4. java基础进阶二:HashMap实现原理分析

    HashMap实现原理分析 1. HashMap的数据结构 数据结构中有数组和链表来实现对数据的存储,但这两者基本上是两个极端. 数组 数组存储区间是连续的,占用内存严重,故空间复杂的很大.但数组的二 ...

  5. 面试官再问你 HashMap 底层原理,就把这篇文章甩给他看

    前言 HashMap 源码和底层原理在现在面试中是必问的.因此,我们非常有必要搞清楚它的底层实现和思想,才能在面试中对答如流,跟面试官大战三百回合.文章较长,介绍了很多原理性的问题,希望对你有所帮助~ ...

  6. 总结HashMap实现原理分析

    一.底层数据结构在JDK1.6,JDK1.7中,HashMap采用位桶+链表实现,即使用链表处理冲突,同一hash值的键值对会被放在同一个位桶里,当桶中元素较多时,通过key值查找的效率较低. 而JD ...

  7. HashMap底层原理及jdk1.8源码解读

    一.前言 写在前面:小编码字收集资料花了一天的时间整理出来,对你有帮助一键三连走一波哈,谢谢啦!! HashMap在我们日常开发中可谓经常遇到,HashMap 源码和底层原理在现在面试中是必问的.所以 ...

  8. JMM和Volatile底层原理分析

    JMM和volatile分析 1.JMM:Java Memory Model,java线程内存模型 JMM:它是一个抽象的概念,描述的是线程和内存间的通信,java线程内存模型和CPU缓存模型类似,它 ...

  9. hashMap 底层原理+LinkedHashMap 底层原理+常见面试题

    1.源码 java1.7    hashMap 底层实现是数组+链表 java1.8 对上面进行优化  数组+链表+红黑树 2.hashmap  是怎么保存数据的. 在hashmap 中有这样一个结构 ...

随机推荐

  1. 自定义校验注解ConstraintValidator

    一 前言 系统执行业务逻辑之前,会对输入数据进行校验,检测数据是否有效合法的.所以我们可能会写大量的if else等判断逻辑,特别是在不同方法出现相同的数据时,校验的逻辑代码会反复出现,导致代码冗余, ...

  2. 仿MSDN的帮助系统

    作为软件开发人员,软件做好后,接下来就是编写文档.我自己也是做软件的,经常有用户询问软件的安装与使用, 我一直很喜欢微软的MSDN帮助系统,简介.大气,使用方便. 网上也找了很久,感觉一直没有合适的, ...

  3. js【生成规定数量不重复随机数】、【冒泡排序】、【鸡尾酒排序】、【选择排序】、【插入排序】、【未完工的二分插入排序】------【总结】

    [生成规定数量不重复随机数] function creatRandom( num ){ var randomLen = num, ranArr = [], thisRan = null, whileO ...

  4. UML第二部分和创建型模式

    状态机视图通过对每个类的对象的生命期进行建模 描述了对象时间上的动态行为 .状态指就某个特定类而言 对于发生的事件具有相同性质响应的一系列对象值.状态机不但可以描述类的行为 而且可以描述用例 协作和方 ...

  5. vue自定义插件封装,实现简易的elementUi的Message和MessageBox

    vue自定义插件封装示例 1.实现message插件封装(类似简易版的elementUi的message) message组件 <template>     <transition  ...

  6. 当初自学C++时的笔记记录

    编辑:刘风琛 最初编写日期:2020年4月11日下午 最新更新日期:2020年9月20日上午 标注: 从笔记开始截止到程序第四章"程序流程结构",使用Joplin编写,其余部分为T ...

  7. Python - 关于类(self/cls) 以及 多进程通讯的思考

    Python-多进程中关于类以及类实例的一些思考 目录 Python-多进程中关于类以及类实例的一些思考 1. 背景 2. Python 类中的函数 - staticmethod / classmet ...

  8. 『Spring Boot 2.4新特性』减少95%内存占用

    节省 95%的内存占用,减少 80%的启动耗时. GraalVM 是一种高性能的虚拟机,它可以显著的提高程序的性能和运行效率,非常适合微服务.最近比较火的 Java 框架 Quarkus 默认支持 G ...

  9. 横趟!面试中遇到的 ZooKeeper 问题

    本文作者:HelloGitHub-老荀 本文是 HelloZooKeeper 系列的最后一篇文章,接下来主要聊聊面试中如果被问到 ZooKeeper 的问题如何回答,也可以当作学完本系列的测试. 准备 ...

  10. Css预编语言以及区别

    一.是什么 Css 作为一门标记性语言,语法相对简单,对使用者的要求较低,但同时也带来一些问题 需要书写大量看似没有逻辑的代码,不方便维护及扩展,不利于复用,尤其对于非前端开发工程师来讲,往往会因为缺 ...