一、ThreadLocal 基本概念
ThreadLocal 是 Java 提供的线程本地变量机制,让每个线程拥有自己独立的变量副本,实现线程隔离。
// 基本使用
ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("线程私有数据");
String value = threadLocal.get();
threadLocal.remove();
二、底层数据结构
┌─────────────────────────────────────────────────────────────┐
│ Thread │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ThreadLocalMap │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ Entry[] table │ │ │
│ │ │ ┌────────────┬────────────┬────────────┐ │ │ │
│ │ │ │ Entry[0] │ Entry[1] │ Entry[2] │ ... │ │ │
│ │ │ │ key(弱引用) │ key(弱引用) │ key(弱引用) │ │ │ │
│ │ │ │ value(强) │ value(强) │ value(强) │ │ │ │
│ │ │ └────────────┴────────────┴────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Entry 源码
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; // 强引用!
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 是弱引用!
value = v;
}
}
private Entry[] table;
}
三、引用关系图解
栈内存 堆内存
┌─────────┐
│ ref │─────强引用────────▶ ┌──────────────────┐
│(局部变量)│ │ ThreadLocal对象 │
└─────────┘ └──────────────────┘
▲
│ 弱引用 (key)
┌────────┴─────────┐
│ Entry │
│ ┌────────────┐ │
│ │ value │──┼──强引用──▶ 实际数据对象
│ └────────────┘ │
└──────────────────┘
│
│ 属于
▼
┌─────────┐ ┌──────────────────┐
│ thread │─────强引用────────▶ │ Thread 对象 │
│(当前线程)│ │ └─threadLocals │──▶ ThreadLocalMap
└─────────┘ └──────────────────┘
四、为什么 Key 用弱引用?
场景分析:如果 Key 是强引用
public void businessMethod() {
ThreadLocal<BigObject> tl = new ThreadLocal<>();
tl.set(new BigObject());
// 业务逻辑...
// 方法结束,tl 局部变量出栈
// 但如果 Entry.key 是强引用:
// Thread → ThreadLocalMap → Entry → ThreadLocal对象
// ThreadLocal 对象永远无法被回收!
}
【Key 强引用的问题】
方法结束后:
┌─────────┐
│ tl │ ← 已出栈,不存在了
└─────────┘
但是引用链仍然存在:
Thread ──强──▶ ThreadLocalMap ──强──▶ Entry ──强──▶ ThreadLocal对象
↑
永远无法回收!
使用弱引用后
【Key 弱引用的好处】
方法结束后:
Thread ──强──▶ ThreadLocalMap ──强──▶ Entry ──弱──▶ ThreadLocal对象
↑
没有强引用了,下次GC被回收
Entry.key 变成 null
结论:Key 用弱引用是为了让 ThreadLocal 对象能被及时回收
五、为什么 Value 不能用弱引用?
ThreadLocal<User> userTL = new ThreadLocal<>();
userTL.set(new User("张三")); // 这个User对象只被Entry.value引用
// 如果value是弱引用:
User user = userTL.get(); // 可能返回null!因为User对象可能已被GC
【Value 弱引用的灾难】
ThreadLocal ───────────────────────┐
│ │
▼ ▼
Entry User对象
key ──弱──▶ ThreadLocal ▲
value ──弱────────────────────┘ ← 唯一引用是弱引用
随时可能被GC!
调用 get() 时,数据可能已经消失 → 程序逻辑错误!
结论:Value 必须是强引用,保证数据在主动删除前不会丢失
六、内存泄漏问题详解
泄漏发生的条件
GC 后的状态:
Thread ──强──▶ ThreadLocalMap ──强──▶ Entry
│
key = null (已被GC)
value ──强──▶ 大对象 ← 无法访问但无法回收!
完整泄漏场景
// 场景:线程池 + ThreadLocal
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.execute(() -> {
ThreadLocal<byte[]> tl = new ThreadLocal<>();
tl.set(new byte[1024 * 1024 * 10]); // 10MB
// 忘记调用 remove()
// tl 出栈,ThreadLocal对象被GC
// 但 10MB 的 byte[] 还被 Entry.value 强引用着!
});
// 线程池中的线程不会销毁
// ThreadLocalMap 一直存在
// Entry.key=null, Entry.value=10MB数据
// 内存泄漏!
图解泄漏过程
【Step 1: 正常状态】
┌─────────┐ 强引用 ┌──────────────┐
│ tl │───────────────▶│ ThreadLocal │
└─────────┘ └──────────────┘
▲
│弱引用
┌──────┴──────┐
Thread ───▶ Map ───▶ Entry │ key │
│ value ───┼──强──▶ [10MB数据]
└─────────────┘
【Step 2: 方法结束,tl出栈】
┌─────────┐
│ tl │ ← 已不存在
└─────────┘
┌──────────────┐
无强引用 ────────▶ │ ThreadLocal │ ← 下次GC被回收
└──────────────┘
▲
│弱引用
┌──────┴──────┐
Thread ───▶ Map ───▶ Entry │ key │
│ value ───┼──强──▶ [10MB数据]
└─────────────┘
【Step 3: GC后 - 内存泄漏状态!】
┌─────────────┐
Thread ───▶ Map ───▶ Entry │ key = null │ ← ThreadLocal已被回收
│ value ──────┼──强──▶ [10MB数据]
└─────────────┘ ↑
无法访问,但无法回收!
内存泄漏!
七、ThreadLocal 的自清理机制
源码中的清理逻辑
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e); // 会清理 stale entry
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i); // 清理 key=null 的 entry
return;
}
}
// ...
}
为什么自清理不可靠?
调用 get()/set() 时才可能触发清理
│
▼
┌─────────────────────────────────────┐
│ 问题1: 如果一直不调用get/set呢? │
│ 问题2: 清理是探测性的,不是全量的 │
│ 问题3: 线程池线程长期存活 │
└─────────────────────────────────────┘
│
▼
无法保证泄漏的内存被及时回收
八、最佳实践
1. 必须使用 try-finally
ThreadLocal<User> userContext = new ThreadLocal<>();
public void process() {
try {
userContext.set(getCurrentUser());
// 业务逻辑
} finally {
userContext.remove(); // 必须清理!
}
}
2. 使用 static 修饰
// 推荐:避免创建多个 ThreadLocal 实例
private static final ThreadLocal<User> USER_CONTEXT = new ThreadLocal<>();
// 不推荐:每次创建新实例
public void method() {
ThreadLocal<User> tl = new ThreadLocal<>(); // 每次new一个
}
3. 完整使用模板
public class UserContextHolder {
private static final ThreadLocal<User> CONTEXT = new ThreadLocal<>();
public static void set(User user) {
CONTEXT.set(user);
}
public static User get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
// 使用时
public void handleRequest(User user) {
try {
UserContextHolder.set(user);
// 业务处理...
} finally {
UserContextHolder.clear();
}
}
九、总结对比表
|-----------|---------------------------|-------------|
| 特性 | Key (弱引用) | Value (强引用) |
| 引用类型 | WeakReference | 普通强引用 |
| 设计目的 | 让 ThreadLocal 对象可被 GC | 保证数据不会意外丢失 |
| GC 行为 | 无强引用时下次 GC 回收 | 只要被引用就不回收 |
| 潜在问题 | key 变 null,形成 stale entry | 导致内存泄漏 |
核心记忆点
1. Key弱引用 → 解决 ThreadLocal 对象泄漏
2. Value强引用 → 保证数据可靠性
3. 两者组合 → 产生新问题:value泄漏
4. 解决方案 → 手动 remove()