Juc篇——深入理解 Java 的 ThreadLocal 及其内存泄漏问题
在多线程开发中,ThreadLocal 是一个非常实用的工具,它为每个线程提供独立的变量副本,从而避免线程间共享数据引发的并发问题。然而,如果使用不当,尤其是在使用线程池的场景下,ThreadLocal 也可能导致内存泄漏。理解这个问题,首先需要掌握 Java 中的四种引用类型。
1. Java 的四种引用类型(前置知识)
Java 中对象引用类型有四种:强引用、软引用、弱引用、虚引用。
-
强引用(Strong Reference)
-
默认引用类型,例如
String str = new String("Hello")。 -
对象只要被强引用持有,就不会被 GC 回收。(宁愿OOM也不回收)
-
GC 不会因为内存不足而回收强引用对象。
-
比喻:对象被“绑着走”,引用消失后对象才能被回收。
-
-
软引用(Soft Reference)
-
通过
SoftReference创建。 -
对象只有在 JVM 内存不足时才会被回收,常用于缓存。
-
比喻:对象像“可怜的小猫”,内存紧张时才会被收走。
-
-
弱引用(Weak Reference)
-
通过
WeakReference创建。 -
GC 一旦发现对象只被弱引用持有,就立即回收。(抗不住gc的毒打)
-
ThreadLocalMap 中的 key 就是弱引用。
-
比喻:对象像“纸片”,GC 来就扫掉。
-
-
虚引用(Phantom Reference)
-
通过
PhantomReference创建。 -
永远不能通过
get()获取对象,只能在对象被 GC 回收时收到通知。 -
主要用于对象回收前的清理工作。
-
比喻:对象像“公告牌”,被 GC 收走前给你发通知,但你拿不到它。
-
2. ThreadLocal 的作用和典型使用场景
ThreadLocal 提供线程级别的数据存储,每个线程都有独立的副本(相当于捆绑,每个线程有一个自己的储物柜),避免线程安全问题。
主要作用:
-
线程内数据传递:在同一个线程执行过程中,ThreadLocal 的数据可以在线程方法链中传递,而无需通过方法参数传递。
-
解决并发问题:为每个线程提供独立的数据副本,避免共享资源的竞争。
典型使用场景:
-
用户身份信息存储:请求拦截器中存储用户 ID、权限信息。
-
线程安全对象:例如每线程独立的
SimpleDateFormat实例。 -
日志上下文存储:每条日志记录当前线程的用户信息。
-
TraceId 存储:分布式链路追踪中存储请求的 TraceId。
-
数据库 Session 管理:ORM 框架中为每个线程管理独立的会话。
3. ThreadLocal 的实现原理
每个线程维护一个 ThreadLocalMap,本质上是一个线程内部的“仓库”。
ThreadLocalMap 结构:
-
Entry:每个 Entry 是一个
<ThreadLocal, value>对-
key:ThreadLocal 对象(弱引用)
-
value:存储的数据(强引用)
-
-
数组存储:Entry 数组用 ThreadLocal 的
threadLocalHashCode快速定位存储位置
ThreadLocal 的基本操作:
-
set():获取当前线程,拿到 ThreadLocalMap,把
this(ThreadLocal 对象)作为 key,value 存入 map -
get():获取当前线程,拿到 ThreadLocalMap,通过
this取出对应 value
public class ThreadLocal<T> {
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
赋:图示底层数据结构展示(一个threadLocalMap对应就是一个Entry数组):
Thread
└── ThreadLocalMap
└── Entry[]
├── Entry[...] → (tl1 弱引用, "A")
├── Entry[...] → (tl2 弱引用, "B")
└── Entry[...] → (tl3 弱引用, "C")

4. ThreadLocal 内存泄漏问题
内存泄漏定义: 对象无法被回收,长期占用内存。
ThreadLocal 内存泄漏的原因:
-
ThreadLocalMap 中的 key 是弱引用,GC 会回收 ThreadLocal 对象
-
value 是强引用,即使 key 被回收,value 仍然存在
-
线程池中线程长期存在,ThreadLocalMap 随线程存活,value 不被回收
示意:
解析:1到2是强引用,随着1的消失,2会被gc清理掉(因为1一旦消失就没有指向2的强引用了),一旦2消失和他捆绑的3就一起消失了(因为捆绑在一起),此时相当于entry中key没了,value还在,也就存在了内存泄露。(因为此时thred对象和value相当于是共生的关系,如果在线程池的背景下,相当于线程不断的被复用,更加容易造成oom了)
解决方案:
使用完 ThreadLocal 后,必须调用 remove() 清理 value,尤其是线程池环境下。
try {
threadLocal.set(value);
// 使用线程局部变量
} finally {
threadLocal.remove(); // 避免内存泄漏
}
5. 总结
-
ThreadLocal为每个线程提供独立变量副本,简化并发安全设计。 -
使用时必须注意线程池环境下的内存泄漏问题,原因是 ThreadLocalMap 中 value 的强引用残留。
-
掌握 强/软/弱/虚引用 的区别,可以更好理解 ThreadLocal 的实现和潜在风险。
-
正确做法是在 finally 中清理 ThreadLocal,以保证数据及时释放。
更多推荐
所有评论(0)