ThreadLocal 和 HashMap 是如何解决哈希碰撞的

哈希碰撞

哈希碰撞也称哈希冲突。

经过哈希计算之后得到哈希值 ,根据哈希值 去找对应的哈希槽。

不同的对象得到的哈希值有可能是一样的,那么就会出现大家都去抢同一个哈希槽 ,都去抢同一个坑位了。这就是哈希碰撞。

为了能让元素在更加均匀、散列 分布在哈希槽中,我们设计了许多解决哈希碰撞的方法。本文将结合 HashMap 和 ThreadLocal 介绍一下拉链法 和开放地址法。

拉链法

在 HashMap 采取的就是 拉链法 来解决哈希冲突。

当发生哈希碰撞的时候,依然会选择在这个槽位,这个槽位会维护一个链表或者其他数据结构(红黑树),会把这个元素添加到里面去。

开放地址法

在 ThreadLocal 中采取的是 开放地址法 的方法来解决哈希冲突。

当遇到哈希冲突时,会再次进行 探测,探测的意思其实就是去寻找下一个空位。

关于这个探测有非常多的实现方式,常见的有:

  • 线性探测:如果当前位置被占用,则依次往后查找,直到找到一个空槽位。
  • 二次探测:如果当前位置被占用,双向地去找。di = i + 1,i - 1, i + 3, i - 3
  • 双重哈希:使用两个哈希函数,根据第一个哈希函数的结果找到一个位置,如果该位置已被占用,则使用第二个哈希函数计算下一个位置,以此类推,直到找到一个空槽位。

在 ThreadLocal 里面采用的时是最简单的 线性探测。

java 复制代码
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                // 这里采用的是线性探测,一直找到空的位置
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

private static int nextIndex(int i, int len) {
    // 步长为1, 一直往右边去找
    return ((i + 1 < len) ? i + 1 : 0);
}

拉链法和开放地址法比较

结构决定功能。我们从他们的结构其实就能非常明显地分析出他们的优缺点。

这里我们从拉链法的角度来讨论。

拉链法适合用于解决频繁产生哈希冲突的场景。

图中采用开放地址法, 23 经过了 3轮探测才能找到他自己的位置,当堆积的元素足够多的时候,这里就是性能的瓶颈了。

然而在拉链法中,并不会产生堆积现象,发生哈希冲突的元素直接会添加到槽位中的集合。

拉链法适用于数据量不可预测的场景。

当元素数量达到一定大小的时候,这里会涉及到 负载因子 (load factor),tab 就要进行扩容。

在容器元素比较多的时候,如果进行一次扩容,而这个扩容又是按 2次幂 进行扩容的(不知道为啥是2次幂的赶紧去补知识),很容易就会产生多余的空间。

然而开放地址法每一个位置只能存一个元素,在相同元素的条件下,开放地址法的 tab 会会比拉链法的 tab 容量要大,所以开放地址法适合可以提前确定数据量的场景,拉链法适合数据量不可预测的场景。

为什么ThreadLocal选择了开放地址法而不是拉链法

看到这里了,其实你也应该隐约知道为什么ThreadLocal采取的是开放地址法而不是拉链法了。

存储 TheadLocal 的 ThreadLocalMap 采取的是:<ThreadLoacl, Object> 这样的存储结构。

ThreadLocal#setInitalValue

java 复制代码
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

我们可以看到他的 key 是 ThreadLocal,相比起 HashMap 的 <Object, Object> 存储结构,ThreadLocalMap 发生哈希碰撞的几率是非常小的。

所以对于 ThreadLocalMap 来说,选择开放地址法会更好。

总结

  • 拉链法适合解决哈希碰撞频繁的场景。
  • 拉链法适合解决数据集不可观测的场景。
  • 拉链法适合解决数据集大的场景。

开放地址法则反之。

来都来了,点个赞再走吧彦祖👍,这对我来说非常重要!

相关推荐
猿究院_夏燊13 分钟前
Spring Bean 生命周期
java·后端·spring
Wang's Blog38 分钟前
Java 项目实战: 交友项目:登录链路回顾与今日任务地图
java·开发语言·状态模式·交友
信誓旦旦的程序猿1 小时前
【Python量化实战宝典 #08】可转债与债券接口手册:转股价·溢价率·纯债价值(Python 可跑)
java·python·股票数据api·股票数据api接口·股票api数据接口·股票量化数据api·股票量化数据接口
砚底藏山河1 小时前
python量化:行情数据回溯
java·开发语言·python·金融·maven
Jlzn88881 小时前
华东电芯分选通道数怎么配才不堵线
java·人工智能·架构·自动化·制造
香吧香2 小时前
GRPC连接机制---长连接分析
java·网络与传输协议
CCblandy2 小时前
雪花 ID 四个边界实测:时钟回拨重号 500 个,前端 JSON.parse 直接丢精度
后端·python·算法·性能优化·架构
我是人✓2 小时前
SpringBoot项目的五种创建方式
java·spring boot·后端
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 阿里云 OSS 入门案例:从创建 Bucket 到上传拿到可访问 URL
java·阿里云·交友
裕晟资质规划3 小时前
三类涉密资质延续时限差异分析:军工60日与集成印制3个月的边界
java·服务器·前端·网络·安全