【并发编程 | 第五篇】探索ThreadLocal的原理

什么是ThreadLocal?

ThreadLocal 是 Java 提供的一个线程本地变量工具类,用于在多线程环境下为每个线程提供独立的变量副本。简单来说,它能让每个线程拥有自己的"储物柜",存储仅对自己可见的数据,避免线程间的数据竞争,同时减少同步锁的开销。

当你创建一个 ThreadLocal 变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 ThreadLocal 名称的由来。线程可以通过 get() 方法获取自己线程的本地副本,或通过 set() 方法修改该副本的值,从而避免了线程安全问题。

ThreadLocal的原理了解吗?

简单看一下ThreadLocal的源码

复制代码
public class Thread implements Runnable {
    //......
    //与此线程有关的ThreadLocal值。由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    //......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 setget方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()set()方法。

看一下ThreadLocal的set方法:

复制代码
public void set(T value) {
    //获取当前请求的线程
    Thread t = Thread.currentThread();
    //取出 Thread 类内部的 threadLocals 变量(哈希表结构)
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 将需要存储的值放入到这个哈希表中
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

由此可以看出,最终的变量是存储在当前线程的ThreadLocalMap上,而并非ThreadLocal上,ThreadLocal可以理解为ThreadLocalMap的封装,传递了变量值。ThreadLocal类可以通过Thread.currentThread()方法获取当前线程对象后,直接通过getMap(Thread)方法可以访问到该线程的ThreadLocalMap对象。

每个Thread中都存在一个ThreadLocalMap对象,而ThreadLocalMap可以存储以ThreadLocal对象为key,以Object为value的键值对。

加入我们在同一个线程中声明了两个ThreadLocal对象,都是仅由Thread中唯一的ThreadLocalMap来存储,以ThreadLocal对象为key,value就是ThreadLocal对象调用的set方法设置的值。

ThreadLocal内存泄漏是怎么导致的?

ThreadLocal内存泄漏的根本原因在于其内部实现机制。

通过上面,我们已经直到,当使用ThreadLocal存储值时,实际上是存储到了ThreadLocalMap中,以ThreadLocal对象为key,以set方法设置的值为value。

要想学习ThreadLocal的内存泄露问题,我们首先要对ThreadLocalMap的引用机制做个了解。

ThreadLocalMap中key和value的引用机制:

key是弱引用: ThreadLocalMap 中的 key 是 ThreadLocal 的弱引用 。 这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null

value是强引用: 即使 key 被 GC 回收,value 仍然被 ThreadLocalMap.Entry 强引用存在,无法被 GC 回收。

打个比方:

想象你有一个 "共享储物柜"(ThreadLocal),每个线程(比如员工)可以往储物柜里存自己的私人物品(value)。但储物柜有个特殊规则:

  • 钥匙(Key)是纸做的(弱引用):如果员工把钥匙弄丢了(ThreadLocal 实例被回收),储物柜管理员(GC)会主动清理钥匙。

  • 储物柜里的物品(Value)是铁盒装的 (强引用):即使钥匙丢了,铁盒还在储物柜里,除非员工自己主动清理(调用 remove())。

  • 问题来了:如果储物柜一直存在(比如线程池中的线程长期存活),而员工频繁更换钥匙但不清除旧铁盒,储物柜里会堆满无法打开的废铁盒(内存泄漏)。

ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,即会造成内存泄漏。

也就是说,内存泄漏的发生需要同时满足两个条件:

  1. ThreadLocal 实例不再被强引用;
  2. 线程持续存活,导致 ThreadLocalMap 长期存在。

如何避免ThreadLocal的内存泄露呢?

  • 在使用完 ThreadLocal 后,务必调用 remove() 方法。 这是最安全和最推荐的做法。 remove() 方法会从 ThreadLocalMap 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 ThreadLocal 定义为 static final,也强烈建议在每次使用后调用 remove()
  • 在线程池等线程复用的场景下,使用 try-finally 块可以确保即使发生异常,remove() 方法也一定会被执行。

如何跨线程传递ThreadLocal的值呢?

在 Java 中,ThreadLocal 的设计初衷是为每个线程提供独立的变量副本,天然不支持跨线程传递值。但实际开发中(如异步任务、线程池场景),常需要将主线程的上下文传递给子线程。

如果想要在异步场景下传递 ThreadLocal 值,有两种解决方案:

  • InheritableThreadLocalInheritableThreadLocal 是 JDK1.2 提供的工具,继承自 ThreadLocal 。使用 InheritableThreadLocal 时,会在创建子线程时,令子线程继承父线程中的 ThreadLocal 值,但是无法支持线程池场景下的 ThreadLocal 值传递。
  • TransmittableThreadLocalTransmittableThreadLocal (简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal类,可以在线程池的场景下支持 ThreadLocal 值传递。

InheritableThreadLocal原理

InheritableThreadLocal 实现了创建异步线程时,继承父线程 ThreadLocal 值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 Thread 类来实现创建线程时,ThreadLocal 值的传递。

InheritableThreadLocal 的值存储在哪里?

Thread 类中添加了一个新的 ThreadLocalMap ,命名为 inheritableThreadLocals ,该变量用于存储需要跨线程传递的 ThreadLocal 值。如下:

复制代码
class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

如何完成 ThreadLocal 值的传递?

通过改造 Thread 类的构造方法来实现,在创建 Thread 线程时,拿到父线程的 inheritableThreadLocals 变量赋值给子线程即可。相关代码如下

复制代码
// Thread 的构造方法会调用 init() 方法
private void init(/* ... */) {
	// 1、获取父线程
    Thread parent = currentThread();
    // 2、将父线程的 inheritableThreadLocals 赋值给子线程
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        	ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

TransmittableThreadLocal原理

JDK 默认没有支持线程池场景下 ThreadLocal 值传递的功能,因此阿里巴巴开源了一套工具 TransmittableThreadLocal 来实现该功能。

阿里巴巴无法改动 JDK 的源码,因此他内部通过 装饰器模式 在原有的功能上做增强,以此来实现线程池场景下的 ThreadLocal 值传递。

TTL 改造的地方有两处:

  • 实现自定义的 Thread ,在 run() 方法内部做 ThreadLocal 变量的赋值操作。

  • 基于 线程池 进行装饰,在 execute() 方法中,不提交 JDK 内部的 Thread ,而是提交自定义的 Thread

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

相关推荐
侠客行031713 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪13 小时前
深入浅出LangChain4J
java·langchain·llm
老毛肚15 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎15 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码15 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚15 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂15 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang16 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐16 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG16 小时前
JavaTuples 库分析
java