
ThreadLocal 是 Java 面试中高级岗位的高频考点。很多人知道它是线程本地变量但面试官一追问为什么会内存泄漏就答不上来了。本文从原理到源码到实际应用帮你把 ThreadLocal 彻底搞懂。一、ThreadLocal 解决什么问题一句话让每个线程拥有自己独立的变量副本线程之间互不干扰。最常见的场景// 每个请求一个线程每个线程需要知道当前登录用户是谁 public class UserContext { private static final ThreadLocalUser currentUser new ThreadLocal(); public static void set(User user) { currentUser.set(user); } public static User get() { return currentUser.get(); } public static void remove() { currentUser.remove(); } }Filter 中拦截请求把用户信息 set 进去Service 层任意位置直接 get不需要一层层传参数。请求结束 remove 掉。不用 ThreadLocal 的话怎么办要么把 User 对象从 Controller 传到 Service 再传到 DAO侵入性强要么放在一个全局 Map 里自己管理线程安全复杂、容易出 bug。二、底层原理Thread、ThreadLocalMap、Entry很多人以为数据存在 ThreadLocal 对象里。不是。数据存在 Thread 对象里。// Thread 类的源码 public class Thread { ThreadLocal.ThreadLocalMap threadLocals null; }每个 Thread 都有一个 ThreadLocalMap这个 Map 的 key 是 ThreadLocal 实例value 是你存的值。调用关系threadLocal.set(value) → 获取当前线程 Thread.currentThread() → 拿到该线程的 ThreadLocalMap → 以 this当前 ThreadLocal 实例为 key存入 value threadLocal.get() → 获取当前线程 Thread.currentThread() → 拿到该线程的 ThreadLocalMap → 以 this 为 key取出 value关键理解ThreadLocal 本身不存数据它只是一个钥匙真正的数据存在每个线程自己的 Map 里。不同线程用同一个 ThreadLocal 对象作为 key但各自的 Map 是独立的所以取到的值不同。三、ThreadLocalMap 的结构ThreadLocalMap 是 ThreadLocal 的静态内部类不是 java.util.HashMap它自己实现了一套static class ThreadLocalMap { static class Entry extends WeakReferenceThreadLocal? { Object value; Entry(ThreadLocal? k, Object v) { super(k); // key 是弱引用 value v; } } private Entry[] table; // 底层数组 }两个关键点1底层是数组不是链表/红黑树。哈希冲突用**开放寻址法**线性探测不是拉链法2key是弱引用WeakReference——这就是内存泄漏问题的根源四、内存泄漏为什么会泄漏怎么避免这是面试最爱问的部分。4.1 弱引用导致的问题栈中的引用强引用→ ThreadLocal 对象 ← Entry.key弱引用Entry.value → 实际数据强引用正常情况下栈中的引用和 Entry 的弱引用同时指向 ThreadLocal 对象。当栈中的引用被回收了比如方法结束、变量置 null栈中的引用已回收 ThreadLocal 对象 ← Entry.key弱引用GC 后变 nullEntry.value → 实际数据强引用还在GC 会回收只有弱引用指向的 ThreadLocal 对象导致 Entry 的 key 变成 null。但 value 是强引用不会被回收。这时候 Entry 就变成了一个key 为 nullvalue 还在的废弃节点。只要线程还活着比如线程池中的线程这个 value 就永远不会被回收——这就是内存泄漏。4.2 ThreadLocal 的自我清理机制ThreadLocal 在 get/set/remove 时会顺便清理 key 为 null 的废弃 Entry源码中叫 expungeStaleEntry。所以如果你一直在调用 ThreadLocal 的方法废弃 Entry 会被逐步清理。但如果你 set 完就再也不碰了废弃 Entry 就一直留着。4.3 正确用法一定要 removetry { threadLocal.set(value); // 业务逻辑 } finally { threadLocal.remove(); // 必须 }特别是在线程池环境下Web 应用几乎都是线程池线程会被复用如果不 remove- 下一个请求可能拿到上一个请求的数据数据串了- 废弃 Entry 越积越多内存泄漏4.4 为什么 key 要用弱引用面试官可能会追问这个。如果 key 是强引用那即使外部不再使用某个 ThreadLocal 对象Entry 的 key 也会阻止它被 GC。这样 ThreadLocal 对象本身也会泄漏。用弱引用至少能保证 ThreadLocal 对象可以被回收泄漏的只是 value。而且 ThreadLocal 的自我清理机制可以逐步回收这些 value。弱引用是一种尽力而为的兜底策略不是完美方案。真正的保障还是要靠手动 remove。五、实际使用场景场景 1Web 应用中传递用户上下文public class RequestContext { private static final ThreadLocalLong userId new ThreadLocal(); private static final ThreadLocalString traceId new ThreadLocal(); // set/get/remove 方法... }在 Filter 或 Interceptor 中 set业务代码中 get请求结束 remove。Spring 的 RequestContextHolder 就是这么做的。场景 2SimpleDateFormat 线程安全问题SimpleDateFormat 不是线程安全的。要么每次 new浪费要么加锁性能差要么用 ThreadLocalprivate static final ThreadLocalSimpleDateFormat sdf ThreadLocal.withInitial(() - new SimpleDateFormat(yyyy-MM-dd));每个线程一份 SimpleDateFormat 实例既线程安全又不浪费。当然 JDK 8 建议直接用 DateTimeFormatter它本身就是线程安全的。场景 3数据库连接 / 事务管理Spring 的事务管理器用 ThreadLocal 存储当前线程的数据库连接保证同一个事务中的多次 SQL 操作用的是同一个 Connection。六、面试回答模板- ThreadLocal 是线程本地变量让每个线程拥有自己独立的变量副本。- 底层实现每个 Thread 对象里有一个 ThreadLocalMapkey 是 ThreadLocal 实例弱引用value 是实际的值。调用 set/get 时先拿到当前线程的 Map再以 ThreadLocal 自身为 key 进行操作。- 关于内存泄漏因为 key 是弱引用当外部不再持有 ThreadLocal 的强引用时GC 会回收 key导致 Entry 的 key 变成 null 但 value 还在。特别是在线程池环境下线程不会销毁这些废弃 Entry 就一直占着内存。所以用完必须调用 remove。- 常见使用场景Web 应用中传递用户上下文、解决 SimpleDateFormat 线程安全问题、Spring 事务管理器存储当前连接等。七、高频追问-ThreadLocalMap 为什么用开放寻址法而不是拉链法→ ThreadLocalMap 的元素通常很少一个线程不会有太多 ThreadLocal 变量开放寻址法在元素少时缓存命中率更高性能更好-InheritableThreadLocal 是什么→ 子线程可以继承父线程的 ThreadLocal 值。但在线程池中不适用因为线程是复用的不是新建的。阿里的 TransmittableThreadLocal 解决了这个问题-ThreadLocal 和 synchronized 的区别→ synchronized 是多个线程竞争同一个资源用锁保证同一时刻只有一个线程访问ThreadLocal 是每个线程一份副本用空间换时间根本不存在竞争【想实战练习这类面试题推荐用 AI 模拟面试逐题对练http://106.12.14.47:8090/ 选 Java 高级难度AI 面试官会像真人一样追问你细节。】