当前位置: 首页 > news >正文

ThreadLocal底层原理解析

ThreadLocal

ThreadLocalJava 中提供的一种 线程局部变量机制。它可以让每个线程都拥有自己独立的变量副本,互不干扰

底层原理

核心机制:每个线程中都有一个 ThreadLocalMap,它会将 ThreadLocal 对象作为键,对应的值作为值进行存储。

结构为:Thread → ThreadLocalMap → Entry → ThreadLocal

在这里插入图片描述

Thread

Thread类有一个ThreadLocalMap,意味着每个线程拥有一个ThreadLocalMap实例

/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap

ThreadLocalMap类是在ThreadLocal类中定义的,它是ThreadLocal类的静态内部类

  • ThreadLocalMap 不需要依赖 ThreadLocal 实例,所以它是静态内部类
  • ThreadLocalMap 不是一个通用的 Map,它专为 ThreadLocal 服务,甚至用 ThreadLocal 实例作为 key。所以它以内部类的形式封装在ThreadLocal类中
public class ThreadLocal<T> {static class ThreadLocalMap {// 自定义 Map 实现,用来存储每个线程的局部变量}
}

Entry

ThreadLocalMap 类中又定义了一个特殊的内部类 Entry,它是ThreadLocalMap中的元素,每个Entry表示一个 ThreadLocal 对象及其值的绑定。

static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}private Entry[] table;
}

这个 Entry 类不是普通的 Map.Entry,它是一个专门用于 ThreadLocalMap 的键值对结构

字段含义
ThreadLocal<?> k (继承自 WeakReference键:ThreadLocal实例,用弱引用包裹
Object value值:ThreadLocal实例对应的值

super(k)会调用父类WeakReference<ThreadLocal<?>>的构造方法将ThreadLocal实例包装成弱引用

为什么 Entry 继承 WeakReference?

一句话:为了解决内存泄漏

如果 ThreadLocalMap.Entry 用的是强引用来保存 key(ThreadLocal 对象),那么一旦用户不再使用这个 ThreadLocalMap 仍然持有这个引用,导致无法被 GC 回收。使用 弱引用(WeakReference<ThreadLocal<?>>,当外部不再持有 ThreadLocal 对象时,GC 可以正常清理它。

举例理解

ThreadLocal<String> t = new ThreadLocal<>();
t.set("hello");
t = null; // 用户代码放弃了引用

Entry拥有的是ThreadLocal实例的弱引用,t拥有的是ThreadLocal实例的强引用;由于此时ThreadLocal实例有强引用不会被回收,t = null之后,t不再拥有ThreadLocal实例的强引用;当GC来临时就会被回收。

内存泄漏

值(value)是强引用,不会自动回收

如果 key == null,说明 ThreadLocal 被回收了,但 value 还在,可能导致内存泄漏

防范措施:

  • 使用 ThreadLocal 后务必调用 remove()

  • ThreadLocalMap 实现中有一个重要的清理机制(expungeStaleEntry),用于清理键为 null(被 GC 回收)的stale entry,防止内存泄漏。它不是显示调用的,调用时机如下:

    ThreadLocal.set()└─► ThreadLocalMap.set()└─► 若发现 key == null└─► expungeStaleEntry(...)ThreadLocal.get()└─► ThreadLocalMap.getEntry()└─► 若 entry.get() == null└─► expungeStaleEntry(...)ThreadLocal.remove()└─► ThreadLocalMap.remove()└─► expungeStaleEntry(...)
    

既然Value会导致内存泄漏,那为什么不把 value 也用弱引用包装?

因为 value 是由业务方控制生命周期的,不能随便被 GC 自动回收,否则会导致程序出错或行为异常

举例说明(假设value是弱引用):

ThreadLocal<String> t = new ThreadLocal<>();
tl.set(new String("hello")); // new 的,没有其他强引用System.gc();
String s = t.get(); // 可能是 null,因为 hello 被 GC 掉了

t还没有使用,里面的值就已经被回收掉了,导致t.get()返回的是null

总结:ThreadLocalMap 只对 key 使用弱引用,是为了避免泄漏;value 保留强引用,是为了让程序数据可控,防止 GC 随意清除业务数据。

ThreadLocalset()get()remove()是如何工作的?

都是先获取当前线程:Thread t = Thread.currentThread()

再获取当前线程的ThreadLocalMapThreadLocalMap map = getMap(t)

然后执行各自的逻辑

如何让子线程继承父线程的 ThreadLocal 变量?

想让子线程继承,可以用:InheritableThreadLocal,它是ThreadLocal 的子类

Thread类中有inheritableThreadLocals属性

/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

当创建子线程时,调用Thread 类的构造函数,在构造函数中,会检查当前线程(父线程)的 inheritableThreadLocals 变量:

if (parent.inheritableThreadLocals != null) {this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

如果父线程有 inheritableThreadLocals,则调用 createInheritedMap() 复制一份给子线程。

关键注意事项

  1. 复制是浅拷贝(引用传递),如果值是可变对象(如 HashMap),父子线程可能修改同一对象。

  2. 线程池中的线程是复用的,子线程创建时只会复制一次父线程的变量。后续任务复用该线程时,不会重新复制。

    ExecutorService executor = Executors.newFixedThreadPool(1);
    inheritableThreadLocal.set("任务1的值");
    executor.submit(() -> System.out.println("任务1: " + inheritableThreadLocal.get())); // 输出 "任务1的值"inheritableThreadLocal.set("任务2的值");
    executor.submit(() -> System.out.println("任务2: " + inheritableThreadLocal.get())); // 仍输出 "任务1的值"!
    
  3. 子线程创建时需要复制父线程的变量,可能影响性能(尤其在大量短生命周期线程场景)。

应用场景

  • 用户会话信息的存储:Web 应用中,每个请求由不同线程处理,需将用户身份(如 ID、Token)与线程绑定
  • 数据库连接管理:在多线程环境下(如 Web 应用),每个线程需要独立的数据库连接,在框架中(如 Spring)通常已内置实现,不建议手写
  • 日志追踪:在分布式系统中,通过 ThreadLocal 存储请求链路 ID(如 TraceID),便于日志关联
http://www.xdnf.cn/news/596125.html

相关文章:

  • 比较结构的连通性
  • MySQL多线程备份工具mysqlpump详解!
  • 骰子游戏(2023睿抗省赛)
  • C++函数封装和绑定
  • 硬件,软件和进程
  • 过氧化物酶的邻近标记技术(APEX):最灵敏的蛋白互作方法
  • Python生成物理引擎的简单知识图谱
  • SOC-ESP32S3部分:6-任务看门狗
  • 101个α因子#18
  • 【Python/Tkinter】实现程序菜单
  • JVM部分内容
  • mybatisplus公共字段自动填充
  • 1.3 任务Task的说明(Xqt)
  • [Linux文件系统] “我的文件在哪?”FHS标准深度解析与核心目录实用指南
  • MVC和MVVM架构的区别
  • sqli-labs——二次注入
  • 常见的慢SQL优化方式
  • strlen和sizeof,const char *、char * const 和char []区别
  • 第二十九天打卡
  • 网络割接的详细流程和关键点
  • 关于常见日志的几种级别和格式
  • 加工生产调度(Johnson算法)
  • vue vite textarea标签按下Shift+Enter 换行输入,只按Enter则提交的实现思路
  • 准备好,开始构建:由 Elasticsearch 向量数据库驱动的 Red Hat OpenShift AI 应用程序
  • 手写ES6 Promise() 相关函数
  • 怎么把https://github.com项目拉到自己的github
  • 在Ubuntu18.04下搭建SadTalker让图片开口说话
  • 第五章:异步幻境 · 时间与数据的秘密
  • STM32之温湿度传感器(DHT11)
  • 纯惯导(INS)的误差来源及其对静态漂移曲线的影响