【并发编程 | 第五篇】探索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

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

相关推荐
ylfhpy2 分钟前
Java面试黄金宝典33
java·开发语言·数据结构·面试·职场和发展·排序算法
乘风!20 分钟前
Java导出excel,表格插入pdf附件,以及实现过程中遇见的坑
java·pdf·excel
小小鸭程序员31 分钟前
Vue组件化开发深度解析:Element UI与Ant Design Vue对比实践
java·vue.js·spring·ui·elementui
南宫生1 小时前
Java迭代器【设计模式之迭代器模式】
java·学习·设计模式·kotlin·迭代器模式
seabirdssss1 小时前
通过动态获取项目的上下文路径来确保请求的 URL 兼容两种启动方式(IDEA 启动和 Tomcat 部署)下都能正确解析
java·okhttp·tomcat·intellij-idea
kill bert2 小时前
第30周Java分布式入门 消息队列 RabbitMQ
java·分布式·java-rabbitmq
穿林鸟3 小时前
Spring Boot项目信创国产化适配指南
java·spring boot·后端
此木|西贝3 小时前
【设计模式】模板方法模式
java·设计模式·模板方法模式
wapicn993 小时前
手机归属地查询Api接口,数据准确可靠
java·python·智能手机·php
hycccccch4 小时前
Springcache+xxljob实现定时刷新缓存
java·后端·spring·缓存