跳过正文
  1. 博客/
  2. 后端/
  3. 框架/

深入解析ThreadLocalMap的开放地址法实现

·4 分钟· ·
后端 框架 Java
目录
84 - 这篇文章属于一个选集。

背景
#

在前面的博客中,我们介绍了ThreadLocal的实现原理,其中最核心的部分就是ThreadLocalMap这个数据结构。我们都知道HashMap是使用红黑树或者链表来解决哈希冲突的,那么ThreadLocalMap底层又是如何处理冲突的呢?

ThreadLocalMap的构造
#

ThreadLocalMap内部实际上也是使用Entry数组来存储数据,这点与HashMap类似。但在处理哈希冲突时,ThreadLocalMap采用了完全不同的策略——开放地址法(Open Addressing)。

什么是开放地址法?
#

开放地址法的核心思想是:当发生哈希冲突时(即目标槽位i已被占用),不是像HashMap那样使用链表或红黑树,而是顺序查找下一个可用的槽位(i+1, i+2,…),直到找到一个空槽位为止。

这种方法在哈希冲突严重时性能会下降,但ThreadLocal通过两个巧妙的设计来减少冲突:

  1. 使用AtomicInteger在类加载时就生成唯一的hashCode
  2. 采用斐波那契散列乘数(0x61c88647)来均匀分布哈希值

斐波那契散列的魔力
#

让我们通过一个示例代码来展示这种哈希算法的精妙之处:

import java.util.concurrent.atomic.AtomicInteger;  
  
public class OpenAddressHashDemo {  
    // 哈希表大小(必须为2的幂)  
    private static final int TABLE_SIZE = 16;  
  
    // 哈希增量(黄金分割数)  
    private static final int HASH_INCREMENT = 0x61c88647;  
  
    // 哈希码生成器(类似ThreadLocal的机制)  
    private static AtomicInteger nextHashCode = new AtomicInteger(0);  
  
    // 生成下一个哈希码  
    private static int nextHashCode() {  
        return nextHashCode.getAndAdd(HASH_INCREMENT);  
    }  
  
    // 计算初始索引  
    private static int getIndex(int hashCode) {  
        return hashCode & (TABLE_SIZE - 1); // 等价于 hashCode % TABLE_SIZE  
    }  
  
    // 开放地址法插入(线性探测)  
    public static void insert(int[] table, int hashCode) {  
        int index = getIndex(hashCode);  
        while (true) {  
            if (table[index] == 0) { // 空槽  
                table[index] = hashCode;  
                System.out.println("哈希码 " + hashCode + " 插入到索引 " + index);  
                return;  
            } else {  
                System.out.println("哈希码 " + hashCode + " 在索引 " + index + " 发生碰撞,探测下一个位置");  
                index = (index + 1) & (TABLE_SIZE - 1); // 线性探测(循环)  
            }  
        }  
    }  
  
    public static void main(String[] args) {  
        int[] table = new int[TABLE_SIZE]; // 哈希表初始化为0  
  
        // 生成并插入多个哈希码  
        for (int i = 0; i < 10; i++) { // 负载因子 10/16 = 0.625  
            int hashCode = nextHashCode();  
            insert(table, hashCode);  
        }  
  
        // 打印最终哈希表状态  
        System.out.println("\n最终哈希表内容:");  
        for (int j = 0; j < TABLE_SIZE; j++) {  
            System.out.println("索引 " + j + ": " + (table[j] != 0 ? table[j] : "空"));  
        }  
    }  
}  

运行结果令人惊叹:

哈希码 0 插入到索引 0  
哈希码 1640531527 插入到索引 7  
哈希码 -1013904242 插入到索引 14  
哈希码 626627285 插入到索引 5  
哈希码 -2027808484 插入到索引 12  
哈希码 -387276957 插入到索引 3  
哈希码 1253254570 插入到索引 10  
哈希码 -1401181199 插入到索引 1  
哈希码 239350328 插入到索引 8  
哈希码 1879881855 插入到索引 15  
  
最终哈希表内容:  
索引 0: 空  
索引 1: -1401181199  
索引 2: 空  
索引 3: -387276957  
索引 4: 空  
索引 5: 626627285  
索引 6: 空  
索引 7: 1640531527  
索引 8: 239350328  
索引 9: 空  
索引 10: 1253254570  
索引 11: 空  
索引 12: -2027808484  
索引 13: 空  
索引 14: -1013904242  
索引 15: 1879881855  

10个值插入到16个槽位的哈希表中,竟然没有发生任何哈希冲突!这就是斐波那契散列乘数的神奇之处。

开放地址法的额外优势
#

在ThreadLocalMap的实现中,开放地址法不仅提供了高效的查找性能,还带来了一个额外的好处:在查找过程中会自动检测并清理过期的Entry(即垃圾回收)。这种设计巧妙地结合了哈希查找和垃圾回收两种功能。

具体来说,当ThreadLocalMap进行get或set操作时,如果探测过程中遇到key为null的Entry(表示ThreadLocal实例已被回收),就会触发清理逻辑。这种"惰性清理"机制既保证了性能,又避免了内存泄漏。

总结
#

ThreadLocalMap采用开放地址法而非链表/红黑树来解决哈希冲突,这种设计选择基于以下考虑:

  1. 高效性:通过斐波那契散列乘数,几乎可以避免冲突,保持O(1)的查找性能
  2. 简洁性:不需要维护复杂的链表或树结构,实现更简单
  3. 功能性:在探测过程中自动检测并清理过期Entry,防止内存泄漏
  4. 空间局部性:线性探测对CPU缓存更友好,可以提高访问速度

这种精妙的设计使得ThreadLocalMap在保持高性能的同时,还能优雅地处理内存管理问题,充分展现了Java并发工具类的设计智慧。

84 - 这篇文章属于一个选集。

相关文章

深入剖析ThreadLocal的内存泄漏问题与弱引用的作用
·5 分钟
后端 框架 Java
背景 # 在之前的探讨中,我们已经了解了如何使用ThreadLocal。接下来,我们将深入探究为什么在实际使用中ThreadLocal无法及时释放内存,必须等到线程结束后才能释放,以及ThreadLocal中的弱引用到底起到了什么作用。
ThreadLocal 真的会导致内存泄漏吗?深入剖析使用场景与最佳实践
·3 分钟
后端 框架 Java
背景 # 在一次代码评审中,同事指出我使用 ThreadLocal 可能会导致内存泄漏,这让我大吃一惊——ThreadLocal 这么常用的工具类怎么会引发内存泄漏呢?于是我开始深入研究这个问题。
大话DDD
·11 分钟
后端 框架 Java
背景 # 什么是DDD,DDD全名 Domain Driven Design,是一种架构设计方法,和我们普通的设计模式有什么区别呢,我们知道设计模式有单例、工厂这些,这些东西只和代码有关,他是一种手法,可以看作是一个小手段,就是类似孔己己的茴香豆7种写法一样
大话Java精度问题
·7 分钟
后端 框架 Java
背景 # 事情的起因是,正当我悠闲的品尝一杯Java Caffe的时候,突然飞书一个加急信息铺面而来,“小张啊,你快看下,线上有个用户用优惠券少付一分钱“
Mockito资料整理
·5 分钟
后端 框架 Java 单元测试
背景 # 网上Mockito 资料我看了一下很多都不够清晰,我总结一下我在使用 Mockito 常用的方法
设计模式探索:从原则到实践
·15 分钟
框架 后端 Java
背景 # 在公司推进DDD中,我发现即使代码按照DDD进行分层,但是底层代码还是阅读性比较差,只不过被分到不同的子服务中。怎么让代码更加整洁规范呢?我觉得可以采用设计模式,所以我花了点时间重新学习了所有的设计模式。