ThreadLocal在大厂经常被涉及和考察,下面我详解ThreadLocal原理@mikechen
一、ThreadLocal核心原理
ThreadLocal的核心作用是:让每个线程都拥有自己独立的一份变量副本。

例如:
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("mikechen");
表面上看,是ThreadLocal保存了数据。
实际上并不是。
真正保存数据的是当前线程对象内部的 ThreadLocalMap。
可以简单理解为:
Thread
│
└── ThreadLocalMap
├── ThreadLocal → Value
├── ThreadLocal → Value
└── ThreadLocal → Value
因此,不同线程调用同一个ThreadLocal时,最终访问的是自己线程内部的Map,所以不会互相干扰。
这也是ThreadLocal实现线程隔离的核心。
二、ThreadLocal到底是怎么工作的?

ThreadLocal最核心的三个方法就是:
set()
get()
remove()
执行set()时,ThreadLocal会获取当前线程,然后找到线程内部的ThreadLocalMap,再以当前ThreadLocal对象作为Key保存Value。
执行get()时,也是先获取当前线程,然后从它自己的ThreadLocalMap中查找对应Value。
而remove()则会主动删除当前ThreadLocal对应的数据。
ThreadLocalMap中的Entry比较特殊:
Entry
├── Key:ThreadLocal弱引用
└── Value:真正保存的数据
这里有一个非常关键的设计:Key是弱引用,而Value是强引用。
为什么这么设计?
因为ThreadLocal对象本身如果已经没有其他地方引用,那么GC时可以回收ThreadLocal对象,避免ThreadLocal对象本身长期存在。
但问题也由此产生。
三、为什么ThreadLocal会导致内存泄漏?

假设代码如下:
ThreadLocal<User> threadLocal = new ThreadLocal<>();
threadLocal.set(new User());
当方法执行结束以后,如果外部已经没有任何地方引用这个ThreadLocal,那么GC可能回收ThreadLocal对象。
此时ThreadLocalMap可能变成:
ThreadLocalMap
Entry
Key → null
Value → User对象
也就是所谓的脏Entry。
问题在于:Key已经没有了,但是Value依然被ThreadLocalMap强引用。
如果当前线程是普通短生命周期线程,那么线程结束后,整个ThreadLocalMap都会被回收,问题通常不大。
但如果是Tomcat、Netty等服务器中的线程池线程,线程可能存活很长时间。
于是就可能出现:
线程长期存在
↓
ThreadLocalMap长期存在
↓
Value长期存在
↓
大量对象无法及时回收
↓
内存占用不断增加
这就是ThreadLocal内存泄漏问题的核心。
需要注意:ThreadLocal并不是“必然导致内存泄漏”。
JDK中的ThreadLocalMap在执行set()、get()等操作时,会尝试清理部分失效Entry。但这种清理并不能保证所有Value立即被清理。
所以最可靠的方式仍然是主动调用remove()。
四、大厂生产环境如何正确使用?

最推荐的方式就是:
try {
threadLocal.set(user);
// 业务代码
} finally {
threadLocal.remove();
}
为什么一定要放在finally?
因为无论业务代码正常执行还是抛出异常,都可以确保ThreadLocal最终被清理。
尤其在线程池环境中,这是非常重要的工程实践。
ThreadLocal比较适合保存:
- 用户上下文
- TraceId
- Request上下文
- 数据库连接相关上下文
- 当前线程的一些临时状态
但不建议在ThreadLocal中保存超大的对象,更不能把ThreadLocal当成全局缓存使用。
从架构师角度来看,ThreadLocal真正的价值并不是“存变量”,而是在共享线程执行环境中实现线程级数据隔离。
记住一句话:
ThreadLocal解决的是线程隔离问题,而remove()解决的是线程生命周期问题。
陈睿|mikechen
10年+大厂架构经验,资深技术专家,就职于阿里等一线大厂。