一句话总结
ThreadLocal 为每个线程提供独立的变量副本,实现线程隔离。每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是存储的值。弱引用设计是为了让 ThreadLocal 对象可以被 GC 回收,但 value 是强引用,如果线程不结束且不调用 remove(),会导致内存泄漏。使用完必须调用 remove()。
初级理解
ThreadLocal 是什么?
ThreadLocal 提供线程局部变量,每个线程访问同一个 ThreadLocal 对象时,获取到的是自己独立的副本,互不干扰。
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal =
ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
new Thread(() -> {
threadLocal.set(1);
System.out.println(threadLocal.get()); // 输出 1
}).start();
new Thread(() -> {
threadLocal.set(2);
System.out.println(threadLocal.get()); // 输出 2
}).start();
// 主线程
System.out.println(threadLocal.get()); // 输出 0(初始值)
}
}
核心方法
| 方法 | 作用 |
| set(T value) | 设置当前线程的变量副本 |
| get() | 获取当前线程的变量副本 |
| remove() | 删除当前线程的变量副本(防止内存泄漏) |
| withInitial() | 设置初始值(Java 8+) |
一句话总结:ThreadLocal = 线程级别的 HashMap,每个线程存取自己的数据,互不干扰。
中级深入
ThreadLocal 底层结构
每个 Thread 对象内部都有一个 ThreadLocalMap:
// Thread 类中的成员变量
public class Thread {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
// ThreadLocalMap 是 ThreadLocal 的静态内部类
static class ThreadLocalMap {
// Entry 继承 WeakReference,key 是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // value 是强引用!
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table; // 数组存储,使用线性探测解决冲突
private int size;
private int threshold;
}
set/get 流程
// ThreadLocal.set() 简化源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null)
map.set(this, value); // key=this(ThreadLocal), value=值
else
createMap(t, value); // 首次使用,创建 ThreadLocalMap
}
// ThreadLocal.get() 简化源码
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) return (T) e.value;
}
return setInitialValue(); // 返回初始值并存入 map
}
为什么 Entry 的 key 用弱引用?
如果 key 是强引用,即使外部的 ThreadLocal 对象被置为 null,由于 ThreadLocalMap 还持有它的强引用,ThreadLocal 对象永远不会被 GC 回收。使用弱引用后,当外部没有强引用指向 ThreadLocal 时,GC 可以回收它。
// 弱引用的好处
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("hello");
tl = null; // 外部强引用断开
// key 是强引用:ThreadLocal 对象不会被 GC → 内存泄漏
// key 是弱引用:ThreadLocal 对象可以被 GC → key 变为 null
// 但 value 仍是强引用!需要 remove() 或 get/set 时清理
中级要点:Thread → ThreadLocalMap → Entry[] → Entry(key=弱引用ThreadLocal, value=强引用)。弱引用解决 key 的回收,但 value 仍需手动清理。
高级拓展
内存泄漏的完整分析
ThreadLocal 内存泄漏的根本原因:
1. key 是弱引用:ThreadLocal 对象被 GC 后,Entry 的 key 变为 null
2. value 是强引用:key 为 null 的 Entry,其 value 永远不会被访问到,但也不会被回收
3. 线程存活时间长:线程池中的线程不会销毁,ThreadLocalMap 一直存在,导致 value 累积
// 内存泄漏场景
public class LeakDemo {
private static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 100; i++) {
pool.execute(() -> {
threadLocal.set(new byte[1024 * 1024]); // 1MB
// 忘记调用 remove()!
// 线程池线程不销毁 → ThreadLocalMap 不销毁 → 内存泄漏
});
}
}
}
ThreadLocal 的自我清理机制
ThreadLocal 在 get/set 时会探测式清理:遇到 key 为 null 的 Entry 时,将其 value 置为 null 并清除 Entry。但这只是被动清理,如果之后不再调用 get/set,泄漏的 Entry 永远不会被清理。
// ThreadLocalMap.set() 中的清理逻辑(简化)
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); // 清理过期 Entry
return;
}
}
tab[i] = new Entry(key, value);
// 检查是否需要 rehash,同时清理过期 Entry
cleanSomeSlots(i, sz);
}
InheritableThreadLocal — 父子线程传递
// ThreadLocal:子线程拿不到父线程的值
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("parent");
new Thread(() -> {
System.out.println(tl.get()); // null!
}).start();
// InheritableThreadLocal:子线程可以继承父线程的值
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("parent");
new Thread(() -> {
System.out.println(itl.get()); // "parent"
}).start();
// 注意:线程池中线程复用,InheritableThreadLocal 只在创建时传递一次
高级加分项:能画出 Thread → ThreadLocalMap → Entry 的引用链,能解释弱引用设计的原因和 value 泄漏的根因,能说出探测式清理的局限性,知道 InheritableThreadLocal 在线程池中的问题。
实战场景
场景一:数据库连接管理
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder =
ThreadLocal.withInitial(() -> {
return DriverManager.getConnection(URL, USER, PWD);
});
public static Connection getConnection() {
return connectionHolder.get();
}
public static void closeConnection() {
Connection conn = connectionHolder.get();
if (conn != null) {
conn.close();
connectionHolder.remove(); // 必须 remove!
}
}
}
场景二:Spring 事务管理
Spring 通过 ThreadLocal 将数据库连接绑定到当前线程,确保同一个事务中的所有 DAO 操作使用同一个连接。
场景三:链路追踪 TraceId
public class TraceContext {
private static ThreadLocal<String> traceIdHolder = new ThreadLocal<>();
public static void setTraceId(String traceId) {
traceIdHolder.set(traceId);
}
public static String getTraceId() {
return traceIdHolder.get();
}
public static void clear() {
traceIdHolder.remove(); // 防止内存泄漏
}
}
常见坑
| 坑 | 原因 | 解决方案 |
| 线程池 + 忘记 remove | 线程复用导致上次的值残留 | 每次使用完必须 remove() |
| InheritableThreadLocal + 线程池 | 线程复用,子线程只在创建时继承 | 用 TransmittableThreadLocal(阿里开源) |
| ThreadLocal 存大对象 | 线程不销毁,大对象一直占用内存 | 及时 remove(),避免存大对象 |
面试模拟
Q:ThreadLocal 的原理是什么?
A:每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是存储的值。调用 get() 时,先获取当前线程的 ThreadLocalMap,再以当前 ThreadLocal 为 key 查找。这样每个线程访问同一个 ThreadLocal 对象时,实际是从自己的 Map 中取值,实现了线程隔离。
Q:ThreadLocal 为什么会内存泄漏?如何解决?
A:Entry 的 key 是弱引用,ThreadLocal 被 GC 后 key 变为 null,但 value 是强引用,且线程池中线程不销毁,ThreadLocalMap 一直存在,导致 value 无法回收。虽然 get/set 时有探测式清理,但不可靠。解决方案:每次使用完必须调用 remove()。
Q:为什么 Entry 的 key 用弱引用?
A:如果 key 是强引用,即使外部 ThreadLocal 对象置为 null,ThreadLocalMap 仍持有强引用,ThreadLocal 永远不会被 GC。弱引用让 ThreadLocal 可以被回收,但 value 的泄漏问题仍需 remove() 解决。这是两害相权取其轻的设计。