阿里二面:说一下Hashmap散列表的三大问题与线程安全问题

本文深入探讨HashMap的底层原理,重点关注散列函数、哈希冲突的解决方案(链地址法和红黑树)、扩容策略以及线程安全问题。HashMap通过高16位与低16位异或提高散列均匀度,并通过控制数组长度为2的整数次幂简化取模运算。在解决冲突时,HashMap采用链表和红黑树,并规定特定条件触发树化。扩容时,HashMap会将容量扩大一倍,利用数组长度为2的整数次幂的特性。HashMap在多线程环境下不保证线程安全,可使用ConcurrentHashMap等解决方案。
摘要由CSDN通过智能技术生成

前言

很高兴遇见你~

HashMap是一个非常重要的集合,日常使用也非常的频繁,同时也是面试重点。本文并不打算讲解基础的使用api,而是深入HashMap的底层,讲解关于HashMap的重点知识。需要读者对散列表和HashMap有一定的认识。

HashMap本质上是一个散列表,那么就离不开散列表的三大问题: 散列函数、哈希冲突、扩容方案 ;同时作为一个数据结构,必须考虑多线程并发访问的问题,也就是 线程安全 。这四大重点则为学习HashMap的重点,也是HashMap设计的重点。

HashMap属于Map集合体系的一部分,同时继承了Serializable接口可以被序列化,继承了Cloneable接口可以被复制。他的的继承结构如下:

HashMap并不是全能的,对于一些特殊的情景下的需求官方拓展了一些其他的类来满足,如线程安全的ConcurrentHashMap、记录插入顺序的LinkHashMap、给key排序的TreeMap等。

文章内容主要讲解四大重点: 散列函数、哈希冲突、扩容方案、线程安全 ,再补充关键的源码分析和相关的问题。

本文所有内容如若未特殊说明,均为JDK1.8版本。

哈希函数

哈希函数的目标是计算key在数组中的下标。判断一个哈希函数的标准是:散列是否均匀、计算是否简单。

HashMap哈希函数的步骤:

  1. 对key对象的 hashcode 进行扰动

  2. 通过取模求得数组下标

扰动是为了让hashcode的随机性更高,第二步取模就不会让所以的key都聚集在一起,提高散列均匀度。扰动可以看到 hash() 方法:

1

2

3

4

5

    static final int hash(Object key) {

    int h;

    // 获取到key的hashcode,在高低位异或运算

    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

也就是低16位是和高16位进行异或,高16位保持不变。一般的数组长度都会比较短,取模运算中只有低位参与散列;高位与地位进行异或,让高位也得以参与散列运算,使得散列更加均匀。具体运算如下图(图中为了方便采用8位进行演示,32位同理):

对hashcode扰动之后需要对结果进行取模。HashMap在jdk1.8并不是简单使用 % 进行取模,而是采用了另外一种更加高性能的方法。HashMap控制数组长度为2的整数次幂,好处是对hashcode进行求余运算和让hashcode与数组长度-1进行位与运算是相同的效果。如下图:

但位与运算的效率却比求余高得多,从而提升了性能。在扩容运算中也利用到了此特性,后面会讲。取模运算的源码看到 putVal() 方法,该方法在 put() 方法中被调用:

1

2

3

4

5

6

7

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

               boolean evict) {

    ...

    // 与数组长度-1进行位与运算,得到下标

    if ((p = tab[i = (n - 1) & hash]) == null)

        ...

}

完整的hash计算过程可以参考下图:

上面我们提到HashMap的数组长度为2的整数次幂,那么HashMap是如何控制数组的长度为2的整数次幂的?修改数组长度有两种情况:

  1. 初始化时指定的长度

  2. 扩容时的长度增量

先看第一种情况。默认情况下,如未在HashMap构造器中指定长度,则初始长度为16。 16是一个较为合适的经验值,他是2的整数次幂,同时太小会频繁触发扩容、太大会浪费空间 。如果指定一个非2的整数次幂,会自动转化成 大于该指定数的最小2的整数次幂 。如指定6则转化为8,指定11则转化为16。结合源码来分析,当我们初始化指定一个非2的整数次幂长度时,HashMap会调用 tableSizeFor() 方法:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

    public HashMap(int initialCapacity, float loadFactor) {

    ...

    this.loadFactor = loadFactor;

    // 这里调用了tableSizeFor方法

    this.threshold = tableSizeFor(initialCapacity);

}

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;

}

tableSizeFor() 方法的看起来很复杂,作用是使得最高位1后续的所有位都变为1,最后再+1则得到刚好大于initialCapacity的最小2的整数次幂数。如下图(这里使用了8位进行模拟,32位也是同理):

那为什么必须要对 cap 进行 -1 之后再进行运算呢?如果指定的数刚好是2的整数次幂,如果没有-1结果会变成比他大两倍的数,如下:

1

00100 --高位1之后全变1--> 00111 --加1---> 01000

第二种改变数组长度的情况是扩容。HashMap每次扩容的大小都是原来的两倍,控制了数组大小一定是2的整数次幂,相关源码如下:

1

2

3

4

5

6

7

8

    final Node<K,V>[] resize() {

    ...

    if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                 oldCap >= DEFAULT_INITIAL_CAPACITY)

            // 设置为原来的两倍

            newThr = oldThr << 1;

    ...

}

小结:

  1. HashMap通过高16位与低16位进行异或运算来让高位参与散列,提高散列效果;

    </
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值