面试常考:ThreadLocalMap 结构、弱引用与内存泄漏、InheritableThreadLocal 的局限、TransmittableThreadLocal
一、从一次线上 OOM 开始
线上服务运行 3 天后突然 OOM,堆 dump 分析发现大量 byte[] 被 ThreadLocal 引用无法回收。线程池中的线程是复用的,ThreadLocal 的值随线程一直活着。ThreadLocal 到底怎么导致内存泄漏?正确用法是什么?
二、ThreadLocal 的数据结构
scss
┌──────────────────────────────────────────────────────────────────┐
│ ThreadLocal 内存结构 │
│ │
│ Thread 对象 │
│ ┌──────────────────────────────────────────┐ │
│ │ ... │ │
│ │ ThreadLocal.ThreadLocalMap threadLocals │ ← 每个线程一个 │
│ └──────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ThreadLocalMap │
│ ┌─────────────────────────────────────┐ │
│ │ Entry[] table │ │
│ │ ┌───────┐ ┌───────┐ ┌───────┐ │ │
│ │ │Entry 0│ │Entry 1│ │Entry 2│ ... │ │
│ │ └───┬───┘ └───┬───┘ └─┬─────┘ │ │
│ └──────┼──────────┼───────┼──────────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌───▼────┐ ┌▼─────────────────┐ │
│ │ WeakRef │ │WeakRef │ │ WeakRef │ │
│ │ ↓ │ │ ↓ │ │ ↓ │ │
│ │ThreadLocal│ThreadLocal│ ThreadLocal │ ← Key (弱引用) │
│ │ │ │ │ │ │ │
│ │ value │ │ value │ │ value │ ← Value(强引用) │
│ │ (byte[])│ │(User) │ │ (Connection) │ │
│ └─────────┘ └────────┘ └──────────────────┘ │
│ │
│ ThreadLocal 对象本身在外部被强引用 (static field) │
└──────────────────────────────────────────────────────────────────┘
核心关系:Thread 持有 ThreadLocalMap,ThreadLocalMap 的 Entry 以 ThreadLocal 为 key、业务对象为 value。
三、源码拆解
Thread
java
public class Thread {
// 每个线程都有自己的 ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
ThreadLocal
java
public class ThreadLocal<T> {
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
}
ThreadLocalMap:自定义的弱引用哈希表
java
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
private int size = 0;
private int threshold;
}
关键设计:Entry 的 key 是弱引用,value 是强引用。
四、内存泄漏分析
正常用法(不泄漏)
ini
外部: static ThreadLocal<User> tl = new ThreadLocal<>();
Thread.threadLocals:
Entry(key=WeakRef(tl), value=User对象)
tl = null;
→ key 被 GC 回收
→ Entry 变成: key=null, value=User对象
→ value 仍被 Entry 强引用,无法回收!
内存泄漏的完整链路
scss
Thread (线程池中,永生)
└→ ThreadLocalMap
└→ Entry[]
└→ Entry
├→ key: WeakRef(ThreadLocal) ← GC后变null
└→ value: BigObject ← 强引用,无法回收!
JDK 的补救措施
java
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 顺带清理其他 key=null 的 Entry
...
}
但这只是补救,不是根治! 如果你 set 后不再 get/set/remove,那个 Entry 永远不会被清理。
正确用法:务必 remove
java
private static final ThreadLocal<Connection> connHolder = new ThreadLocal<>();
public void executeQuery(String sql) {
try {
Connection conn = getConnection();
// 使用 conn
} finally {
connHolder.remove(); // 必须 finally 中 remove!
}
}
五、ThreadLocalMap 的哈希冲突处理:开放寻址法
java
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); return; }
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
六、InheritableThreadLocal:父子线程传递
java
public class InheritableThreadLocalDemo {
private static InheritableThreadLocal<String> context = new InheritableThreadLocal<>();
public static void main(String[] args) {
context.set("main-data");
new Thread(() -> {
System.out.println(context.get()); // "main-data"
}).start();
}
}
InheritableThreadLocal 的致命局限
csharp
InheritableThreadLocal 的传递发生在 Thread 构造时:
新线程创建时复制父线程的数据 → 一次性快照
线程池场景:
worker线程早已创建 → 不会触发 init → 不会复制!
七、TransmittableThreadLocal:线程池场景的终极方案
java
public class TTLDemo {
private static TransmittableThreadLocal<String> requestId = new TransmittableThreadLocal<>();
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(2);
pool = TtlExecutors.getTtlExecutor(pool);
requestId.set("REQ-001");
pool.submit(() -> System.out.println(requestId.get()));
}
}
TTL 原理
markdown
主线程:
TTL.set("REQ-001")
提交任务 → TtlRunnable.get(runnable, ttl)
1. 捕获当前线程所有 TTL 的值 (快照)
2. 包装成 TtlRunnable
线程池执行:
1. 回放: 把快照设置到当前 worker 的 TTL
2. 执行原任务
3. 恢复: 恢复到执行前状态
八、典型应用场景
1. 请求级上下文传递
java
public class RequestContext {
private static final ThreadLocal<RequestContext> CONTEXT = new ThreadLocal<>();
public static RequestContext current() {
RequestContext ctx = CONTEXT.get();
if (ctx == null) { ctx = new RequestContext(); CONTEXT.set(ctx); }
return ctx;
}
public static void clear() { CONTEXT.remove(); }
}
2. 数据库连接管理
java
public class ConnectionManager {
private static final ThreadLocal<Connection> holder = new ThreadLocal<>();
public static Connection getConnection() throws SQLException {
Connection conn = holder.get();
if (conn == null || conn.isClosed()) {
conn = DriverManager.getConnection(URL, USER, PASS);
holder.set(conn);
}
return conn;
}
public static void close() {
Connection conn = holder.get();
if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} holder.remove(); }
}
}
3. SimpleDateFormat 线程安全
java
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> FORMATTER =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) { return FORMATTER.get().format(date); }
public static Date parse(String str) throws ParseException { return FORMATTER.get().parse(str); }
}
九、面试高频问题速答
Q1: ThreadLocal 的 key 为什么用弱引用?
如果 key 是强引用:外部不再使用 ThreadLocal(tl=null)后,Thread→ThreadLocalMap→Entry→key(强引用)→ThreadLocal,ThreadLocal 无法被 GC。用弱引用后,外部无强引用时 ThreadLocal 可被 GC,key 变 null,降低泄漏风险。
Q2: key 用弱引用就能避免内存泄漏吗?
不能。key 回收后 Entry 变成 {null, value},value 仍被强引用。如果线程不死亡(线程池场景),这些 value 永远不会被回收。必须手动 remove() 才能彻底避免泄漏。
Q3: ThreadLocal 和 synchronized 有什么区别?
| 维度 | ThreadLocal | synchronized |
|---|---|---|
| 目的 | 线程隔离 | 线程同步 |
| 方式 | 每个线程一份副本 | 多线程共享同一份 |
| 竞争 | 无竞争 | 有竞争 |
| 适用 | 线程上下文、避免共享 | 保护共享数据 |
Q4: InheritableThreadLocal 为什么在线程池中无效?
InheritableThreadLocal 在子线程创建时复制父线程数据。线程池中 worker 线程预先创建,提交任务时不会创建新线程,因此不会触发复制。需要用 TransmittableThreadLocal + TtlExecutors 解决。
Q5: ThreadLocalMap 为什么用开放寻址而不是链表法?
ThreadLocal 的数量通常不多(几十个以内),开放寻址法在小数据量下缓存友好性更好,没有额外的 Node 对象开销。而且 ThreadLocal 的 hash 是 magicNumber * 0x61c88647(黄金分割数),分布均匀,冲突概率低。
Q6: ThreadLocalMap 的扩容阈值是 2/3?
是的,ThreadLocalMap 负载因子是 2/3。threshold = len * 2 / 3。超过 threshold 且没有清理到 stale entry 时触发 rehash,先清理所有 key=null 的 Entry,如果清理后仍超过 threshold 的 3/4,则扩容(2 倍)。
十、总结
csharp
ThreadLocal 核心知识:
结构: Thread.threadLocals → ThreadLocalMap → Entry(WeakRef<ThreadLocal>, value)
泄漏根因: key 弱引用 → 被GC → key=null; value 强引用 → 无法回收; 线程池场景 → 线程不死亡 → 永久泄漏
解决方案: 用完必须 remove() (finally块)
InheritableThreadLocal: 线程创建时复制 → 线程池场景失效
TransmittableThreadLocal: 任务提交时快照 → 执行前回放 → 执行后恢复
使用原则: 1. static final 2. 用完一定 remove 3. 避免存大对象 4. 线程池场景用 TTL
ThreadLocal 是一把双刃剑------简单优雅的线程隔离方案,但使用不当就是内存泄漏的温床。牢记 finally { threadLocal.remove(); } 是面试加分项,更是生产环境的保命符。