HashMap源码解析超详细

news2025/3/1 11:36:19

HashMap源码详解

  • 1、概述
  • 2、源码解析
    • 1.HashMap底层存储结构
      • 问题一: 为什么直接就用数组呢?
      • 问题二:什么是红黑树呢?
      • 问题三:为什么不一下子把整个链表变为红黑树呢?
    • 2.HashMap的重要成员变量
    • 3.构造方法解析
    • 4.Put方法解析
      • 取模运算 (n - 1) & hash
    • 5.Get方法解析
    • 6.resize 扩容方法解析
    • 7.remove 方法解析
  • 3、Jdk7-扩容死锁分析
  • 4、最后总结

1、概述

HashMap是Map中最为常用的一种,在面试和工作中也经常会被问到相关的问题。由于HashMap数据结构较为复杂,回答相关问题的时候往往不尽人意,尤其是在JDK1.8之后,又引入了红黑树结构,其数据结构变的更加复杂,本文就JDK1.8源码为例,对HashMap进行分析;

只要耐心看完本篇HashMap源码解析,我相信你一定可以告诉别人精通HashMap源码。当然了,如果有什么问题也欢迎提出来,一起进步。下面直接上硬核

2、源码解析

1.HashMap底层存储结构

在JDK1.7的时候只有数组加链表的,但是在JDK1.8之后使用了数组加链表加红黑树。也是为了提升性能。看下面的图可能比较直观:
在这里插入图片描述
每一个节点就是一个Node,Node有四个属性;

Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash; //hash值
            this.key = key;
            this.value = value;
            this.next = next; //下一个指针
        }

问题一: 为什么直接就用数组呢?

  1. 数组我们都知道,在内存中是一块连续的空间,所以如果数组很大的情况下,会很耗费内存。
  2. HashMap是通过hash取值落到数组上的,那么就会有一个问题,没错HASH碰撞,如果两个元素的HASH值一样,那么他们落在同一个槽位上怎么办,所以就有了链表,如果链表足够长,就会变为红黑树,下面会详细剖析这些。

问题二:什么是红黑树呢?

红黑树是一个自平衡的二叉查找树,也就是说红黑树的查找效率是非常的高,查找效率会从链表的o(n)降低为o(logn)。如果之前没有了解过红黑树的话,也没关系,你就记住红黑树的查找效率很高就OK了。

问题三:为什么不一下子把整个链表变为红黑树呢?

这个问题的意思是这样的,就是说我们为什么非要等到链表的长度大于等于8的时候,才转变成红黑树?在这里可以从两方面来解释

(1)构造红黑树要比构造链表复杂,在链表的节点不多的时候,从整体的性能看来, 数组+链表+红黑树的结构可能不一定比数组+链表的结构性能高。就好比杀鸡焉用牛刀的意思。

(2)HashMap频繁的扩容,会造成底部红黑树不断的进行拆分和重组,这是非常耗时的。因此,也就是链表长度比较长的时候转变成红黑树才会显著提高效率。

OK,到这里相信我们对hashMap的底层数据结构有了一个认识。下面我们看一下一些重要的成员变量

2.HashMap的重要成员变量

1. DEFAULT_INITIAL_CAPACITY = 1 << 4;  Hash表默认初始容量 16
位运算:0000 0001  左移四位后 0001 0000  变成了2的四次方=16
2. MAXIMUM_CAPACITY = 1 << 30;  最大Hash表容量,Int的最大值
3. DEFAULT_LOAD_FACTOR = 0.75f; 缺省负载因子(装逼可以说泊松分推导的)
4. TREEIFY_THRESHOLD = 8;链表转红黑树阈值
5. UNTREEIFY_THRESHOLD = 6;树降级称为链表的阈值
6. MIN_TREEIFY_CAPACITY = 64;树化的另一个参数,当哈希表中的所有元素个数超过64时,才会允许树化
7. transient int size;当前哈希表中元素个数
8. transient int modCount;当前哈希表结构修改次数(替换不算)
9. int threshold;扩容阈值,当你的哈希表中的元素超过阈值时,触发扩容(capacity * loadFactor).
10.final float loadFactor;0.75 默认就是上面的 DEFAULT_LOAD_FACTOR(当负载因子较大时,去给table数组扩容的可能性就会少,所以相对占用内存较少(空间上较少),但是每条entry链上的元素会相对较多,查询的时间也会增长(时间上较多)。反之就是,负载因子较少的时候,给table数组扩容的可能性就高,那么内存空间占用就多,但是entry链上的元素就会相对较少,查出的时间也会减少。所以才有了负载因子是时间和空间上的一种折中的说法。所以设置负载因子的时候要考虑自己追求的是时间还是空间上的少。(一般情况下不需要设置,系统给的默认值已经比较适合了)

3.构造方法解析

HashMap 的构造方法不多,只有四个。HashMap 构造方法做的事情比较简单,一般都是初始化一些重要变量,比如 loadFactor(负载因子0.75) 和 threshold(扩容阈值)。而底层的数据结构则是延迟到插入键值对时再进行初始化。HashMap 相关构造方法如下:

/** 构造方法 1 */
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

/** 构造方法 2 */
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/** 构造方法 3 */
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);
}

/** 构造方法 4 */
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

上面4个构造方法中,大家平时用的最多的应该是第一个了。第一个构造方法很简单,仅将 loadFactor 变量设为默认值。构造方法2调用了构造方法3,而构造方法3仍然只是设置了一些变量。构造方法4则是将另一个 Map 中的映射拷贝一份到自己的存储结构中来,这个方法不是很常用。

上面就是对构造方法简单的介绍,构造方法本身并没什么太多东西,所以就不说了。接下来详细说说构造方法 3中给threshold(扩容阈值)是怎么赋值的。方法tableSizeFor

/**
 * 作用:返回一个大于等于当前值cap的一个数字,并且这个数字一定是2的次方数
 */
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的幂。至于为啥要这样,后面再解释。我们先来看看 tableSizeFor 方法的解释:
在这里我举个列子,仔细看应该可以,如果cap=10;
那么n=10-1=9; 如果对无符号右移运算符不了解的,可以看一下这篇文章了解一下
下面我也把位运算的几个基本内容贴一下:
在这里插入图片描述

与:两个是1才是1
或:只要有一个是1就是1
异或:只有11跟或不同,其他一样。(更好的记忆方式:相同是0)
取反:0变1,1变0

下面我们仔细看看上面怎么算的
9的二进制为0b1001

n |= n >>> 1; 无符号右移一位1001前面加10后面舍去最后1位得0b0100
0b1001 | 0b0100 => 0b1101
n |= n >>> 2;无符号右二位1101前面加20后面舍去最后2位得0b0100
0b1101 | 0b0011 => 0b1111
n |= n >>> 4;
0b1111 | 0b0000 => 0b1111
n |= n >>> 8;
0b1111 | 0b0000 => 0b1111
n |= n >>> 16;
0b1111 | 0b0000 => 0b1111
你会发现到最后其实都是一样的了。
那么n=0b1111 转换为10进制为:
(1*20次方=1)+(1*21次方=2)+(1*22次方=4)+(1*23次方=8)=1+2+4+8=15
(n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
n=15不小于0,并且不大于容量的最大值,则n=n+1=16。
但是一般不用这个构造方法

另外,需要注意一下的是,第一步 int n = cap - 1; 这个操作,执行这个操作的主要原因是为了防止在cap已经是2的n次幂的情况下,经过运算后得到的结果是cap的二倍的结果,例如如果n为16,经过一系列运算之后,得到的结果是0001 1111,此时最后一步n+1 执行之后,就会返回32,有兴趣的可以自己进行尝试;

4.Put方法解析

先看图,看懂图再看源码
在这里插入图片描述

/*
 * 作用:让key的hash值的高16位也参与路由运算
 * 异或:相同则返回0,不同返回1
*/
 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

先说第一步怎么计算Hash值的
这段代码叫做扰动函数,也是hashMap中的hash运算,主要分为下面几步:

  • key.hashCode(),获取key的hashCode值,如果不进行重写的话返回的是根据内存地址得到的一个int值
  • key.hashCode() 获取到的hashcode无符号右移16位并和元hashCode进行^ ,这样做的目的是为了让高位与低进行混合,让两者都参与运算,以便让hash值分布更加均匀
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
如果key不为空,并且取到k的hash值为:
h1 = 0b 0010 0101 1010 1100 0011 1111 0010 1110  将它无符号右移16位后做异或运算
^
h2 = 0b 0000 0000 0000 0000 0010 0101 1010 1100
  => 0b 0010 0101 1010 1100 0001 1010 1000 0010
  主要目的就是为了让hash值更加分散不重复

下面看主要得put源码,直接贴代码了,我把注释也写在代码了,耐心读下去。在最后面我会把源码注释文件地址贴出来。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        //tab:引用当前hashMap的散列表
        //p:表示当前散列表的元素
        //n:表示散列表数组的长度
        //i:表示路由寻址 结果
        Node<K,V>[] tab; Node<K,V> p; int n, i;

        //延迟初始化逻辑,第一次调用putVal时会初始化hashMap对象中的最耗费内存的散列表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //最简单的一种情况:寻址找到的桶位 刚好是 null,这个时候,直接将当前k-v=>node 扔进去就可以了
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //e:不为null的话,找到了一个与当前要插入的key-value一致的key的元素
            //k:表示临时的一个key
            Node<K,V> e; K k;

            //表示桶位中的该元素,与你当前插入的元素的key完全一致,表示后续需要进行替换操作
            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 {
                //链表的情况,而且链表的头元素与我们要插入的key不一致。
                for (int binCount = 0; ; ++binCount) {
                    //条件成立的话,说明迭代到最后一个元素了,也没找到一个与你要插入的key一致的node
                    //说明需要加入到当前链表的末尾
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //条件成立的话,说明当前链表的长度,达到树化标准了,需要进行树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            //树化操作 在里面还判断了,需要table的数组长度大于64才会树化
                            treeifyBin(tab, hash);
                        break;
                    }
                    //条件成立的话,说明找到了相同key的node元素,需要进行替换操作
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }

            //e不等于null,条件成立说明,找到了一个与你插入元素key完全一致的数据,需要进行替换
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }

        //modCount:表示散列表结构被修改的次数,替换Node元素的value不计数
        ++modCount;
        //插入新元素,size自增,如果自增后的值大于扩容阈值,则触发扩容。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

从代码看,put方法分为三种情况:

  • table尚未初始化,对数据进行初始化
  • table已经初始化,且通过hash算法找到下标所在的位置数据为空,直接将数据存放到指定位置
  • table已经初始化,且通过hash算法找到下标所在的位置数据不为空,发生hash冲突(碰撞),发生碰撞后,会执行以下操作:

判断插入的key如果等于当前位置的key的话,将 e 指向该键值对
如果此时桶中数据类型为 treeNode,使用红黑树进行插入
如果是链表,则进行循环判断, 如果链表中包含该节点,跳出循环,如果链表中不包含该节点,则把该节点插入到链表末尾,同时,如果链表长度超过树化阈值(TREEIFY_THRESHOLD)且table容量超过最小树化容量(MIN_TREEIFY_CAPACITY),则进行链表转红黑树(由于table容量越小,越容易发生hash冲突,因此在table容量<MIN_TREEIFY_CAPACITY 的时候,如果链表长度>TREEIFY_THRESHOLD,会优先选择扩容,否则会进行链表转红黑树操作)

取模运算 (n - 1) & hash

在hashMap的代码中,在很多地方都会看到类似的代码:

first = tab[(n - 1) & hash]) 

hash算法中,为了使元素分布的更加均匀,很多都会使用取模运算,在hashMap中并没有使用hash%n这样进行取模运算,而是使用(n - 1) & hash进行代替,原因是在计算机中,&的效率要远高于%;需要注意的是,只有容量为2的n次幂的时候,(n - 1) & hash 才能等效hash%n,这也是hashMap 初始化初始容量时,无论传入任何值,都会通过tableSizeFor(int cap) 方法转化成2的n次幂的原因,这种巧妙的设计真的很令人惊叹;
至于为什么只有2的n次幂才能这样进行取模运算,这里就不再详细叙述了

5.Get方法解析

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
final Node<K,V> getNode(int hash, Object key) {
        //tab:引用当前hashMap的散列表
        //first:桶位中的头元素
        //e:临时node元素
        //n:table数组长度
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

        if ((tab = table) != null && (n = tab.length) > 0 &&
                (first = tab[(n - 1) & hash]) != null) {
            //第一种情况:定位出来的桶位元素 即为咱们要get的数据
            if (first.hash == hash && // always check first node
                    ((k = first.key) == key || (key != null && key.equals(k))))
                return first;

            //说明当前桶位不止一个元素,可能 是链表 也可能是 红黑树
            if ((e = first.next) != null) {
                //第二种情况:桶位升级成了 红黑树
                if (first instanceof TreeNode)//
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //第三种情况:桶位形成链表
                do {
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;

                } while ((e = e.next) != null);
            }
        }
        return null;
    }

get方法相对于put来说,逻辑实在是简单太多了

  1. 根据hash值查找到指定位置的数据
  2. 校验指定位置第一个节点的数据是key是否为传入的key,如果是直接返回第一个节点,否则继续查找第二个节点
  3. 如果数据是TreeNode(红黑树结构),直接通过红黑树查找节点数据并返回
  4. 如果是链表结构,循环查找所有节点,返回数据
  5. 如果没有找到符合要求的节点,返回null

6.resize 扩容方法解析

resize方法逻辑比较复杂,需要静下心来一步步的分析,但是总的下来,分为以下几步:

  • 首先先判断当前table是否进行过初始化,如果没有进行过初始化,此处就解决了调用无参构造方法时候,threshold和initialCapacity 未初始化的问题,如果已经初始化过了,则进行扩容,容量为原来的二倍
  • 扩容后创建新的table,并对所有的数据进行遍历

如果新计算的位置数据为空,则直接插入
如果新计算的位置为链表,则通过hash算法重新计算下标,对链表进行分组
如果是红黑树,则需要进行拆分操作

/*
*为什么需要扩容?为了解决哈希冲突导致的链化影响查询效率的问题,扩容会缓解该问题。
*/
final Node<K,V>[] resize() {
        //oldTab:引用扩容前的哈希表
        Node<K,V>[] oldTab = table;
        //oldCap:表示扩容之前table数组的长度
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        //oldThr:表示扩容之前的扩容阈值,触发本次扩容的阈值
        int oldThr = threshold;
        //newCap:扩容之后table数组的大小
        //newThr:扩容之后,下次再次触发扩容的条件
        int newCap, newThr = 0;

        //条件如果成立说明 hashMap中的散列表已经初始化过了,这是一次正常扩容
        if (oldCap > 0) {
            //扩容之前的table数组大小已经达到 最大阈值后,则不扩容,且设置扩容条件为 int 最大值。
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }

            //oldCap左移一位实现数值翻倍,并且赋值给newCap, newCap 小于数组最大值限制 且 扩容之前的阈值 >= 16
            //这种情况下,则 下一次扩容的阈值 等于当前阈值翻倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                    oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        //oldCap == 0,说明hashMap中的散列表是null
        //1.new HashMap(initCap, loadFactor);
        //2.new HashMap(initCap);
        //3.new HashMap(map); 并且这个map有数据
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;

        //oldCap == 0,oldThr == 0
        //new HashMap();
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;//16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
        }

        //newThr为零时,通过newCap和loadFactor计算出一个newThr
        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;

        //说明,hashMap本次扩容之前,table不为null
        if (oldTab != null) {

            for (int j = 0; j < oldCap; ++j) {
                //当前node节点
                Node<K,V> e;
                //说明当前桶位中有数据,但是数据具体是 单个数据,还是链表 还是 红黑树 并不知道
                if ((e = oldTab[j]) != null) {
                    //方便JVM GC时回收内存
                    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;
                        //高位链表:存放在扩容之后的数组的下表位置为 当前数组下标位置 + 扩容之前数组的长度  index=15=>index=31
                        Node<K,V> hiHead = null, hiTail = null;

                        Node<K,V> next;
                        do {
                            next = e.next;
                            //hash-> .... 1 1111
                            //hash-> .... 0 1111
                            // 0b 10000

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

7.remove 方法解析

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
    }
final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        //tab:引用当前hashMap中的散列表
        //p:当前node元素
        //n:表示散列表数组长度
        //index:表示寻址结果
        Node<K,V>[] tab; Node<K,V> p; int n, index;

        if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
            //说明路由的桶位是有数据的,需要进行查找操作,并且删除

            //node:查找到的结果
            //e:当前Node的下一个元素
            Node<K,V> node = null, e; K k; V v;

            //第一种情况:当前桶位中的元素 即为 你要删除的元素
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                node = p;


            else if ((e = p.next) != null) {
                //说明,当前桶位 要么是 链表 要么 是红黑树

                if (p instanceof TreeNode)//判断当前桶位是否升级为 红黑树了
                    //第二种情况
                    //红黑树查找操作,下一期再说
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    //第三种情况
                    //链表的情况
                    do {
                        if (e.hash == hash &&
                                ((k = e.key) == key ||
                                        (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }


            //判断node不为空的话,说明按照key查找到需要删除的数据了
            if (node != null && (!matchValue || (v = node.value) == value ||
                    (value != null && value.equals(v)))) {

                //第一种情况:node是树节点,说明需要进行树节点移除操作
                if (node instanceof TreeNode)
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);

                //第二种情况:桶位元素即为查找结果,则将该元素的下一个元素放至桶位中
                else if (node == p)
                    tab[index] = node.next;

                else
                    //第三种情况:将当前元素p的下一个元素 设置成 要删除元素的 下一个元素。
                    p.next = node.next;

                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

从源码可以看出来,通过key找到需要移除的元素操作过程和get方法几乎一致,最后在查找到key对应的节点之后,根据节点的位置和类型,进行相应的移除操作就完成了,过程非常简单

3、Jdk7-扩容死锁分析

在1.7的时候采用的是头插法,如果在多线程的情况下,发生了扩容,就会出现死锁问题。
单线程是没问题的
先来看看1.7的扩容方法代码;

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.next = newTable[i];//第三行
            newTable[i] = e;//第四行
            e = next;//第五行
        }
    }
}

去掉了一些冗余的代码, 层次结构更加清晰了。

  • 第一行:记录oldhash表中e.next
  • 第二行:rehash计算出数组的位置(hash表中桶的位置)
  • 第三行:e要插入链表的头部, 所以要先将e.next指向new hash表中的第一个元素
  • 第四行:将e放入到new hash表的头部
  • 第五行: 转移e到下一个节点, 继续循环下去

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

while(null != e) {
      Entry<K,V> next = e.next;//第一行,线程1执行到此被调度挂起
      int i = indexFor(e.hash, newCapacity);//第二行
      e.next = newTable[i];//第三行
      newTable[i] = 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)了:
  4. 现在的 e 节点是 key(7),首先执行Entry<K,V> next = e.next,那么 next 就是 key(3)了
  5. 执行e.next = newTable[i],于是key(7) 的 next 就成了 key(3)
  6. 执行newTable[i] = e,那么线程1的新 Hash 表第一个元素变成了 key(7)
  7. 执行e = next,将 e 指向 next,所以新的 e 是 key(3)

此时状态为:在这里插入图片描述
然后又该执行 key(7)的 next 节点 key(3)了:
1.现在的 e 节点是 key(3),首先执行Entry<K,V> 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)
这时候的状态如图所示:
在这里插入图片描述
很明显,环形链表出现了。

Java8 HashMap扩容跳过了Jdk7扩容的坑,对源码进行了优化,采用高低位拆分转移方式,避免了链表环的产生。

4、最后总结

到这里,hashMap的源码基本就解析完成了,其余的方法和源码逻辑相对非常简单,大部分还是使用上述代码来实现的,例如containsKey(jey),就是使用get方法中的getNode()来判断的,由于篇幅原因就不一一介绍。
另外,中间有很部分不影响逻辑理解的代码被一笔带过,比如 红黑树的转化,查找,删除等操作,有兴趣的可以自己进行学习,不过还有一些其他的特性需要提醒一下:

  • HashMap 底层数据结构在JDK1.7之前是由数组+链表组成的,1.8之后又加入了红黑树;链表长度小于8的时候,发生Hash冲突后会增加链表的长度,当链表长度大于8的时候,会先判读数组的容量,如果容量小于64会先扩容(原因是数组容量越小,越容易发生碰撞,因此当容量过小的时候,首先要考虑的是扩容),如果容量大于64,则会将链表转化成红黑树以提升效率
  • hashMap 的容量是2的n次幂,无论在初始化的时候传入的初始容量是多少,最终都会转化成2的n次幂,这样做的原因是为了在取模运算的时候可以使用&运算符,而不是%取余,可以极大的提上效率,同时也降低hash冲突
  • HashMap是非线程安全的,在多线程的操作下会存在异常情况(如形成闭环(1.7),1.8已修复闭环问题,但仍不安全),可以使用HashTable或者ConcurrentHashMap进行代替

篇幅比较长,希望你可以耐心看下去,将军赶路,不追野兔。
如果有理解不对的地方还请指正。

带注释源码地址: com.example.sync.HashMap.HashMap

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

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

相关文章

渗透测试工具库-收藏版

1.前言 浩二一开始做渗透测试的时候收集超多的资料和工具&#xff0c;一直在文档里吃灰。今天全部放出来分享给大家&#xff0c;需要的自己收藏。 2.漏洞练习平台 WebGoat漏洞练习平台&#xff1a; https://github.com/WebGoat/WebGoat webgoat-legacy漏洞练习平台: http…

好程序员:Java书籍推荐,程序员必看的5本Java书籍,赶紧收藏!

今天好程序员给大家推荐5本Java书籍&#xff0c;各大高校都在使用&#xff08;具体名单如下&#xff09;&#xff0c;所有学习Java的程序员都不应该错过&#xff01; 第一本Java书籍《Java EE&#xff08;SSM框架&#xff09;企业应用实战》 本书全面介绍了JavaEE中MyBatis、Sp…

ChatGLM-6B论文代码笔记

ChatGLM-6B 文章目录ChatGLM-6B前言一、原理1.1 优势1.2 实验1.3 特点&#xff1a;1.4 相关知识点二、实验2.1 环境基础2.2 构建环境2.3 安装依赖2.4 运行2.5 数据2.6 构建前端页面3 总结前言 Github&#xff1a;https://github.com/THUDM/ChatGLM-6B 参考链接&#xff1a; ht…

“QT 快速上手指南“ 之 计算器(二)

文章目录前言一、QT 基本组件用法介绍&#xff1a;1. QLabel &#xff1a;2. QPushButton :3. QLineEdit:二、坐标系统三、窗口部件的大小设置1. setSize( ) 函数&#xff1a;2. resize( )函数&#xff1a;3. setFixedSize( )函数:4. setFixedWidth( ) 和 setFixedHeight( )函数…

大一被忽悠进了培训班

大家好&#xff0c;我是帅地。 最近我的知识星球开始营业&#xff0c;不少大一大二的小伙伴也是纷纷加入了星球&#xff0c;并且咨询的问题也是五花八门&#xff0c;反正就是&#xff0c;各种迷茫&#xff0c;其中有一个学弟&#xff0c;才大一&#xff0c;就报考培训班&#…

命令注入概述

概述命令注入即 Command Injection。是指在开发需求中&#xff0c;需要调用一些系统的命令来完成某些特定的功能。当未对用户输入的参数进行严格的过滤时&#xff0c;则有可能发生命令注入。攻击者可以通过提交恶意构造的参数破坏命令语句结构&#xff0c;从而达到执行恶意命令…

九大经典算法

1. 冒泡排序&#xff08;Bubble Sort&#xff09; 两个数比较大小&#xff0c;通过两两交换&#xff0c;像水中的泡泡一样&#xff0c;较大的数下沉&#xff0c;较小的数冒起来。 算法描述&#xff1a; 1.比较相邻的元素。如果第一个比第二个大&#xff0c;就交换它们两个&a…

界面组件DevExtreme v22.2 - Pivot Grid、UI组件升级

DevExtreme拥有高性能的HTML5 / JavaScript小部件集合&#xff0c;使您可以利用现代Web开发堆栈&#xff08;包括React&#xff0c;Angular&#xff0c;ASP.NET Core&#xff0c;jQuery&#xff0c;Knockout等&#xff09;构建交互式的Web应用程序。从Angular和Reac&#xff0c…

【Golang入门】简介与基本语法学习

下面是一篇关于Golang新手入门的博客&#xff0c;记录一下。&#xff08;如果有语言基础基本可以1小时入门&#xff09; 一、什么是Golang&#xff1f; Golang&#xff08;又称Go&#xff09;是一种由谷歌公司开发的编程语言。它是一种静态类型、编译型、并发型语言&#xff0…

从Hive源码解读大数据开发为什么可以脱离SQL、Java、Scala

从Hive源码解读大数据开发为什么可以脱离SQL、Java、Scala 前言 【本文适合有一定计算机基础/半年工作经验的读者食用。立个Flg&#xff0c;愿天下不再有肤浅的SQL Boy】 谈到大数据开发&#xff0c;占据绝大多数人口的就是SQL Boy&#xff0c;不接受反驳&#xff0c;毕竟大…

离散数学_第二章:基本结构:集合、函数、序列、求和和矩阵(1)

集合与函数2.1 集合 2.1.1 集合的基本概念 2.1.2 集合的表示方法 2.1.3 文氏图 2.1.4 证明集合相等 2.1.5 集合的大小 ——基 2.1.6 幂集 2.1.7 集族、指标集 2.1.8 笛卡尔积 2.1.9 容斥原理2.1 集合 2.1.1 集合的基本概念 定义1&#xff1a;集合 是不同对象的一个无序的聚…

23黑马vue

1、Vue概述 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-width, …

铁路轨道不平顺数据分析与预测

铁路轨道不平顺数据分析与预测 1.引言 铁路轨道作为铁行车的基础设施&#xff0c;是铁路线路的重要组成部分。随着经济和交通运输业的发展&#xff0c;我国的铁路运输正朝着高速和重载方向迅速发展&#xff0c;与此同时&#xff0c;轨道结构承受来自列车荷载、运行速度的冲击…

Elasticsearch 学习+SpringBoot实战教程(三)

需要学习基础的可参照这两文章 Elasticsearch 学习SpringBoot实战教程&#xff08;一&#xff09; Elasticsearch 学习SpringBoot实战教程&#xff08;一&#xff09;_桂亭亭的博客-CSDN博客 Elasticsearch 学习SpringBoot实战教程&#xff08;二&#xff09; Elasticsearch …

QT笔记——QtXlsx操作Execl

使用第三方库QtXlsx来操作execl 环境&#xff1a;vs2019 qt5.12.2 第一步&#xff1a; 我们需要找到对应的下载QtXlsx的第三方库的下载路径&#xff1a; QtXlsx下载 第二步&#xff1a; 编译我们的QtXlsx&#xff0c;看了网上有很多中的例子&#xff0c;我这边暂时只举例 vsqt…

一个评测模型+10个问题,摸清盘古、通义千问、文心一言、ChatGPT的“家底”!...

‍数据智能产业创新服务媒体——聚焦数智 改变商业毫无疑问&#xff0c;全球已经在进行大模型的军备竞赛了&#xff0c;“有头有脸”的科技巨头都不会缺席。昨天阿里巴巴内测了通义千问&#xff0c;今天华为公布了盘古大模型的最新进展。不久前百度公布了文心一言、360也公布了…

技术经济学(刘秋华)(第三版)——第三章 经济型评价的基本要素

第三章 经济型评价的基本要素 技术经济分析的基本原理包括技术经济比较原理和资金报酬原理。技术经济比较原理又包括满足需要的可比原理、总消耗费用的可比原理、价格指标的可比原理和时间因素的可比原理四个方面。这些原理的作用在于确保技术方案的经济性和可行性。 其中&am…

ChatGPT批量翻译-ChatGPT批量生成多国语言

ChatGPT翻译的准吗 ChatGPT是一种基于Transformer架构的自然语言处理技术&#xff0c;其翻译准确性取决于所训练的模型和数据集的质量。在特定的语料库和训练数据下&#xff0c;ChatGPT可以实现一定程度的准确翻译。但是&#xff0c;与人工翻译相比&#xff0c;ChatGPT的翻译质…

【Deep Learning】CNN卷积神经网络—卷积

卷积 文章目录卷积Quickstart一&#xff0c;计算机视觉&#xff08;Computer vision&#xff09;二&#xff0c;边缘检测&#xff08;Edge detection example&#xff09;-2.1 卷积操作过程&#xff1a;三&#xff0c;边缘检测的更多知识&#xff08;More edge detection&#…

4月想跳槽的同学,没有更好的选择,可以去美团

在美团干了半年&#xff0c;说一下自己的感受&#xff0c;美团是一家福利中等&#xff0c;工资待遇中上&#xff0c;高层管理团队强大&#xff0c;加班强度一般&#xff0c;技术不错&#xff0c;办公环境一般&#xff0c;工作氛围中上&#xff0c;部门差距之间工作体验差距巨大…