
核心原理数据隔离与内存模型ThreadLocal的核心思想是“以空间换时间”通过为每个线程提供独立的变量副本从根本上避免了多线程环境下的数据竞争和同步开销。1. 数据存储结构钥匙与保险箱一个常见的误解是认为ThreadLocal自身存储了数据。实际上ThreadLocal仅扮演一个“钥匙”或“访问器”的角色真正的数据存储在每个线程Thread对象内部。Thread类每个Thread实例都持有一个ThreadLocalMap类型的成员变量threadLocals。ThreadLocalMap这是ThreadLocal的一个静态内部类一个高度定制化的哈希表。它以ThreadLocal实例本身作为 Key以线程需要存储的变量副本作为 Value。┌─────────────────────────────────────────────────────────────┐ │ Thread A (线程A) │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ threadLocals: ThreadLocalMap │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Key: tl1 │ → │ Value: A-1│ │ │ │ │ │ Key: tl2 │ → │ Value: 100 │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────┐ │ Thread B (线程B) │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ threadLocals: ThreadLocalMap │ │ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Key: tl1 │ → │ Value: B-1│ │ │ │ │ │ Key: tl2 │ → │ Value: 200 │ │ │ │ │ └─────────────┘ └─────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ▲ ▲ │ │ ┌────────┴────────┐ ┌─────────┴─────────┐ │ ThreadLocal tl1 │ │ ThreadLocal tl2 │ │ (一把钥匙) │ │ (另一把钥匙) │ └─────────────────┘ └───────────────────┘当你调用threadLocal.set(value)时其内部流程是获取当前线程对象t。获取t内部的ThreadLocalMap。以当前ThreadLocal实例为 Key将value存入这个 Map 中。get()和remove()方法同理都是操作当前线程自己的ThreadLocalMap。这种设计天然保证了线程间的数据隔离。源码级深度分析public class Thread implements Runnable { /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals null; }注意threadLocals初始值为null只有在第一次调用set()时才会被创建这是一种懒加载设计。set() 方法数据是如何存进去的public void set(T value) { // 1. 获取当前线程 Thread t Thread.currentThread(); // 2. 获取当前线程内部的 ThreadLocalMap ThreadLocalMap map getMap(t); if (map ! null) { // 3. Map 已存在直接以 this (当前 ThreadLocal 实例) 为 Key 存入 map.set(this, value); } else { // 4. Map 不存在第一次 set创建一个新的 Map createMap(t, value); } } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals new ThreadLocalMap(this, firstValue); }流程总结threadLocal.set(hello)→ 找到当前线程 → 拿到线程的 Map →map.put(这个threadLocal实例, hello)get() 方法数据是如何取出来的public T get() { Thread t Thread.currentThread(); ThreadLocalMap map getMap(t); if (map ! null) { // 以当前 ThreadLocal 实例为 Key从当前线程的 Map 中取值 ThreadLocalMap.Entry e map.getEntry(this); if (e ! null) { SuppressWarnings(unchecked) T result (T)e.value; return result; } } // Map 不存在或 Key 不存在返回初始值 return setInitialValue(); } private T setInitialValue() { T value initialValue(); // 默认返回 null可被子类重写 Thread t Thread.currentThread(); ThreadLocalMap map getMap(t); if (map ! null) map.set(this, value); else createMap(t, value); return value; } // 可被重写提供默认初始值 protected T initialValue() { return null; }代码演示一把钥匙多份数据public class ThreadLocalDemo { // 定义两把钥匙 private static ThreadLocalString userContext new ThreadLocal(); private static ThreadLocalInteger requestCount ThreadLocal.withInitial(() - 0); public static void main(String[] args) throws InterruptedException { Runnable task () - { String threadName Thread.currentThread().getName(); // 每个线程用自己的钥匙存自己的数据 userContext.set(用户- threadName); requestCount.set(requestCount.get() 1); System.out.println(threadName | userContext userContext.get()); System.out.println(threadName | requestCount requestCount.get()); // 模拟业务处理 try { Thread.sleep(100); } catch (InterruptedException e) {} // 再次获取仍然是自己的数据 System.out.println(threadName | 处理后 userContext userContext.get()); // 用完记得清理防止内存泄漏后面详述 userContext.remove(); requestCount.remove(); }; // 启动三个线程 new Thread(task, 线程A).start(); new Thread(task, 线程B).start(); new Thread(task, 线程C).start(); } } ----------------------------------------------------------------------------------- 线程A | userContext 用户-线程A 线程B | userContext 用户-线程B 线程C | userContext 用户-线程C 线程A | requestCount 1 线程B | requestCount 1 线程C | requestCount 1 线程A | 处理后 userContext 用户-线程A 线程B | 处理后 userContext 用户-线程B 线程C | 处理后 userContext 用户-线程C2.ThreadLocalMap的定制化设计ThreadLocalMap与HashMap在设计上有显著不同这些差异是理解其高级特性的关键。首先要打破一个固有印象——ThreadLocalMap 根本没有实现java.util.Map接口它只是借用了Map这个名字本质上是一个为 ThreadLocal 量身定制的、极度轻量化的专用数据结构。特性ThreadLocalMapHashMap(JDK 8)哈希冲突解决线性探测法 (开放定址)数组 链表 / 红黑树Key 的引用类型弱引用 (WeakReference)强引用设计目标轻量级适配线程私有、数据量小的场景通用键值对存储线性探测法由于ThreadLocal通常存储的变量不多几个到十几个数据量小线性探测法在缓存友好性和性能上优于链表法。ThreadLocalMap 底层是一个Entry[]数组。当两个 ThreadLocal 实例的哈希值冲突时它不像 HashMap 那样在同一个位置挂一条链表而是往后挨个找空位// 计算初始位置 int i key.threadLocalHashCode (len - 1); // 如果该位置已被占用就往后找 (i1) % len for (Entry e tab[i]; e ! null; e tab[i nextIndex(i, len)]) { // 找到空位就插入 }线性探测法除了前面提到的缓存友好性还有一个关键原因线性探测法天然支持探测路径上的过期 Entry 清理。// set() 方法中的探测循环 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) { // 探测过程中发现过期 Entry立即清理 replaceStaleEntry(key, value, i); return; } }如果用链地址法过期的 Entry 会散落在各个链表中无法在探测路径上顺路清理。而线性探测法让每一次读写操作都变成了内存治理的机会——只要路过过期 Entry就顺手清理并把后面的有效 Entry 往前挪缩短后续探测距离---------------------------------------------------------------------------------------------------------------------弱引用 Key这是ThreadLocalMap最精妙也最危险的设计直接关联到内存泄漏问题。static class Entry extends WeakReferenceThreadLocal? { Object value; Entry(ThreadLocal? k, Object v) { super(k); // Key 是弱引用 value v; // Value 是强引用 } } //设想一下如果 Key 是强引用 public void someMethod() { ThreadLocalString tl new ThreadLocal(); tl.set(hello); // 方法结束tl 局部变量消失 // 但 ThreadLocalMap 里还强引用着这个 ThreadLocal 实例 // → 这个 ThreadLocal 永远无法被 GC 回收 }哈希冲突解决方式和扩容机制与 HashMap 有何不同ThreadLocalMap使用线性探测法解决冲突而HashMap使用链地址法。在扩容上ThreadLocalMap会先清理过期Entry如果清理后元素数量仍超过阈值的 3/4 才会扩容这是一种优化策略旨在避免不必要的扩容开销。对比维度线性探测法ThreadLocalMap链地址法HashMap内存开销无额外指针只有数组每个节点需要 next 指针CPU 缓存数组连续存储缓存命中率极高链表节点分散缓存不友好适用场景数据量小、冲突少数据量大、冲突多代码复杂度简单需要处理链表→红黑树转换黄金分割哈希从源头减少冲突ThreadLocal 的哈希码生成方式非常精妙private static final int HASH_INCREMENT 0x61c88647; // 斐波那契散列的魔数 private static AtomicInteger nextHashCode new AtomicInteger(); private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } private final int threadLocalHashCode nextHashCode();每创建一个 ThreadLocal 实例哈希码就增加0x61c88647。这个魔数是黄金分割数的近似值它的神奇之处在于连续创建的 ThreadLocal 实例在 2 的幂次方长度的数组中哈希分布极其均匀从源头上就把冲突概率压到了最低。扩容机制ThreadLocalMap vs HashMap这是两者差异最大的地方也是 ThreadLocalMap 最精妙的优化所在。HashMap 的扩容逻辑简单粗暴size threshold (容量 × 负载因子 0.75) → 直接扩容为 2 倍 → 重新哈希所有元素ThreadLocalMap 的扩容逻辑两步判断先清理再决定// set() 方法末尾 int sz size; if (!cleanSomeSlots(i, sz) sz threshold) rehash();cleanSomeSlots()会以对数复杂度扫描部分槽位清理过期的 Entry。如果清理到了数据说明数组里腾出了空间可能就不需要扩容了。private void rehash() { // 先全量清理所有过期 Entry expungeStaleEntries(); // 清理后再次判断只有 size threshold * 3/4 才真正扩容 if (size threshold - threshold / 4) resize(); }阶段阈值计算具体数值说明初始 thresholdlen * 2/316 * 2/3 ≈ 10size ≥ 10 时触发 rehashrehash 中的二次判断threshold * 3/410 * 3/4 7.5清理后 size ≥ 8 才扩容扩容后新容量len * 232扩容为 2 倍设计精髓很多时候数组里堆积了大量过期 EntryKey 为 null清理之后有效数据可能只有 5 个远低于阈值 10。如果像 HashMap 那样直接扩容就是浪费。ThreadLocalMap 的做法是先打扫房间打扫完发现空间够了就不需要搬家了。对比维度ThreadLocalMapHashMap触发条件size threshold且清理后仍 threshold * 3/4size threshold扩容前是否清理是先expungeStaleEntries()否扩容倍数2 倍2 倍重新哈希用线性探测法重新定位重新计算 hash链表/红黑树重组设计哲学尽量避免不必要的扩容简单直接达到阈值就扩3. 内存泄漏的根源与规避根本原因ThreadLocalMap中的Entry对 Key (ThreadLocal实例) 是弱引用但对 Value (存储的数据) 是强引用。┌─────────────────────────────────────────────┐ │ 线程Thread—— 一直活着线程池复用 │ │ ┌───────────────────────────────────────┐ │ │ │ 背包ThreadLocalMap —— 跟着线程一起活 │ │ │ │ ┌───────────────────────────────────┐ │ │ │ │ │ 盒子Entry │ │ │ │ │ │ Key: ThreadLocal (弱引用) │ │ │ │ │ │ Value: byte[1MB] (强引用) ←─────┼──┼──┤ │ │ └───────────────────────────────────┘ │ │ │ └───────────────────────────────────────┘ │ └─────────────────────────────────────────────┘// ThreadLocalMap 内部的 Entry 类 static class Entry extends WeakReferenceThreadLocal? { Object value; // 这就是强引用 Entry(ThreadLocal? k, Object v) { super(k); // Key 是弱引用 value v; // Value 是强引用只要我还指着你你就不能被 GC 回收 } }Key被包装在WeakReference里 → 弱引用 → GC 来了就回收Value就是一个普通的成员变量Object value→ 强引用 → 只要 Entry 活着Value 就活着泄漏场景当ThreadLocal实例的外部强引用被置为null后在下一次 GC 时Entry的 Key 会被回收变为null。然而Entry的 Value 仍然被ThreadLocalMap强引用着。如果这个Thread是一个长期存活的线程例如线程池中的核心线程那么这个Value对象将永远无法被回收导致内存泄漏。public class MemoryLeakDemo { public static void main(String[] args) throws InterruptedException { // 创建一个线程池线程会被复用不会销毁 ExecutorService executor Executors.newFixedThreadPool(1); for (int i 0; i 1000; i) { executor.submit(() - { // 每次创建一个新的 ThreadLocal 实例 ThreadLocalbyte[] tl new ThreadLocal(); // 存入一个 1MB 的大对象 tl.set(new byte[1024 * 1024]); // 方法结束tl 这个局部变量超出作用域 // 但由于线程还在运行ThreadLocalMap 中的 Entry 仍然存在 // Key 会被 GC 回收弱引用但 Value (1MB 数组) 仍然是强引用无法回收 }); } Thread.sleep(10000); // 等待观察内存 executor.shutdown(); } }因为线程不死 → 线程身上的threadLocals字段即 Map就不会被销毁 → Map 里所有的 Entry 也都还在。线程不死 → Map 不死 → Entry 不死 → Entry 里的强引用 Value 不死 → 内存泄漏try { threadLocal.set(someValue); // ... 业务逻辑 ... } finally { // 必须在 finally 中清理确保无论如何都会执行 threadLocal.remove(); }解决方案强制规范在使用完ThreadLocal后必须在finally块中显式调用remove()方法。这是唯一可靠的解决方案。兜底机制ThreadLocalMap的set()、get()方法在内部会进行“启发式清理”尝试清理一些 Key 为null的陈旧Entry。但这只是辅助不能替代主动的remove()。最佳实践将ThreadLocal定义为private static final避免频繁创建实例从源头上减少 Key 被回收的概率。题外话什么是引用在 Java 中引用就是指向一个对象的指针。你写Object obj new Object()obj就是一个引用它指向堆内存中的那个 Object 实例。引用类型强度GC 的态度强引用Strong Reference最强只要还有强引用指着你就绝不回收软引用Soft Reference较弱内存不足时才回收弱引用Weak Reference更弱下次 GC 时不管内存够不够都回收虚引用Phantom Reference最弱随时可能回收几乎不用// 强引用只要 obj 还在new Object() 就不会被回收 Object obj new Object(); obj null; // 把引用设为 null对象才可能被回收 // 弱引用即使 weakRef 还指着对象下次 GC 也会把对象回收 WeakReferenceObject weakRef new WeakReference(new Object()); // 此时new Object() 已经没有强引用指向它了 // 下次 GC 一来它就被回收了weakRef.get() 会返回 null4.父子线程传递的演进与陷阱ThreadLocal的线程隔离特性在需要上下文传递的场景下如链路追踪的traceId、用户登录信息成了痛点。为此Java 提供了InheritableThreadLocal但它存在致命缺陷。底层基石Thread 类的双 Map 结构要理解整个上下文传递体系必须先看清java.lang.Thread类的底层结构。每个 Thread 对象内部维护了两个ThreadLocalMappublic class Thread implements Runnable { // ThreadLocal 使用的 Map ThreadLocal.ThreadLocalMap threadLocals null; // InheritableThreadLocal 使用的 Map ThreadLocal.ThreadLocalMap inheritableThreadLocals null; }这两个 Map 完全独立互不干扰。普通 ThreadLocal 操作的是threadLocals而 InheritableThreadLocal 通过重写getMap()和createMap()方法将操作重定向到了inheritableThreadLocals。1.InheritableThreadLocal(ITL)一次性的传递InheritableThreadLocal是ThreadLocal的子类它允许子线程在创建时继承父线程的变量副本一次性浅拷贝。原理Thread类中除了threadLocals还有一个inheritableThreadLocals成员变量。在创建子线程时Thread的构造方法会检查父线程的inheritableThreadLocals如果不为空则会将其中的数据进行浅拷贝并赋值给子线程。致命缺陷这种传递仅在子线程创建时发生一次。如果父线程在子线程创建后修改了值子线程无法感知。更重要的是在线程池场景下由于线程是复用的子线程早已创建完毕因此 ITL 完全失效。public class InheritableThreadLocalT extends ThreadLocalT { // 重定向到 inheritableThreadLocals ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } void createMap(Thread t, T firstValue) { t.inheritableThreadLocals new ThreadLocalMap(this, firstValue); } // 子类可重写此方法实现值的转换 protected T childValue(T parentValue) { return parentValue; } }ITL 的传递逻辑只且仅发生在new Thread()的那一刻。追踪源码调用链new Thread(runnable) → Thread.init() → Thread parent currentThread() // 获取父线程 → if (parent.inheritableThreadLocals ! null) → this.inheritableThreadLocals ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)createInheritedMap()的核心逻辑是遍历父线程的 Entry 数组逐个浅拷贝private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable parentMap.table; int len parentTable.length; table new Entry[len]; for (int j 0; j len; j) { Entry e parentTable[j]; if (e ! null) { ThreadLocalObject key (ThreadLocalObject) e.get(); if (key ! null) { // 调用 childValue()默认直接返回原值浅拷贝 Object value key.childValue(e.value); Entry c new Entry(key, value); // 线性探测法插入 int h key.threadLocalHashCode (len - 1); while (table[h] ! null) h nextIndex(h, len); table[h] c; size; } } } }两个致命缺陷缺陷一传递只在创建时发生一次InheritableThreadLocalString itl new InheritableThreadLocal(); itl.set(v1); Thread child new Thread(() - { System.out.println(itl.get()); // 输出 v1 }); child.start(); itl.set(v2); // 父线程修改值 // 子线程已经创建完毕无法感知到 v2缺陷二线程池场景完全失效这是最致命的。线程池中的线程是预先创建、长期复用的时间线 T0: 线程池启动 → 创建 worker-1, worker-2, worker-3此时父线程的 ITL 为空 T1: 请求A到达 → 父线程 set(traceId-A) → submit(task-A) → worker-1 执行 worker-1 早在 T0 就创建好了init() 时拷贝的是空值 → task-A 拿不到 traceId T2: 请求B到达 → 父线程 set(traceId-B) → submit(task-B) → worker-1 再次执行 worker-1 的 inheritableThreadLocals 里还是空值或残留的旧值→ 数据丢失/污染核心原因ITL 只在创建那一刻抄一次清单。线程池的任务早就set好了所以永远抄不到最新的数据2.TransmittableThreadLocal(TTL)线程池场景的终极方案为了解决 ITL 在线程池中的失效问题阿里开源了TransmittableThreadLocal(TTL)它成为了异步和微服务场景下的标准解决方案。┌─────────────────────────────────────────────────────────────┐ │ TransmittableThreadLocal (继承 InheritableThreadLocal) │ │ └─ 负责存储和管理要传递的值 │ │ │ │ TtlRunnable / TtlCallable (装饰器) │ │ └─ 包装原始任务在 run()/call() 中嵌入 CRR 逻辑 │ │ │ │ Transmitter (工具类) │ │ └─ 提供 capture()、replay()、restore() 三个核心方法 │ │ │ │ TtlExecutors (线程池装饰器) │ │ └─ 自动将提交的 Runnable/Callable 包装为 TtlRunnable │ └─────────────────────────────────────────────────────────────┘核心原理TTL 在任务提交给线程池时进行“增强”。它会捕获父线程当前所有的 TTL 变量并将这些上下文“附着”在任务Runnable/Callable上。当线程池中的工作线程执行这个任务时TTL 会先将捕获的父线程上下文注入到工作线程中任务执行完毕后再恢复工作线程原有的上下文从而实现了上下文在复用线程间的精准传递。使用方式通常使用TtlExecutors.getTtlExecutorService()对原生线程池进行装饰。ITL 仅在子线程创建时进行一次性数据拷贝。线程池中的线程是预先创建并复用的因此无法触发拷贝逻辑。TTL 通过在任务提交时捕获父线程上下文并在任务执行前后进行上下文的注入与恢复从而解决了线程复用带来的传递问题。CRR三步详情第一步Capture捕获—— 任务提交时当父线程调用executorService.submit(task)时TTL 拦截这个操作快照当前线程所有的 TTL 变量// TtlCallable 构造函数 private TtlCallable(NonNull CallableV callable, boolean releaseTtlValueReferenceAfterCall) { // 捕获当前线程父线程所有 TTL 变量的快照 this.capturedRef new AtomicReference(Transmitter.capture()); this.callable callable; this.releaseTtlValueReferenceAfterCall releaseTtlValueReferenceAfterCall; }Transmitter.capture()的内部逻辑遍历当前线程的inheritableThreadLocals数组找到所有 key 是TransmittableThreadLocal实例的 Entry将它们的值深拷贝通过copy()方法保存到一个HashMapTransmittableThreadLocal, Object中返回这个快照对象第二步Replay重放—— 任务执行前当线程池中的工作线程拿到任务准备执行时TTL 将捕获的快照注入到当前工作线程中public V call() throws Exception { final Object captured capturedRef.get(); // 将快照中的值重放到当前工作线程同时备份工作线程原有的值 final Object backup Transmitter.replay(captured); try { return callable.call(); // 执行真正的业务逻辑 } finally { // 恢复工作线程原有的值 Transmitter.restore(backup); } }Transmitter.replay()的内部逻辑先备份当前工作线程原有的 TTL 值用于后续恢复将快照中的每个 TTL 值 set 到当前线程的inheritableThreadLocals中返回备份的旧值第三步Restore恢复—— 任务执行后任务执行完毕后TTL 将工作线程的 TTL 状态恢复到执行前的样子finally { Transmitter.restore(backup); // 恢复工作线程原有的上下文 }Transmitter.restore()的内部逻辑将备份的旧值重新 set 回当前线程如果旧值为 null则调用 remove() 清理为什么 CRR 能解决线程池问题用具体场景说明线程池中有 worker-1早已创建inheritableThreadLocals 为空 请求AtraceIdA到达 父线程 set(A) submit(task-A) → TTL 捕获快照 {traceId: A} → 包装为 TtlCallable worker-1 执行 TtlCallable.call() → replay: 将 {traceId: A} 注入 worker-1 → worker-1 拿到 A → 执行业务逻辑能正确获取 traceIdA → restore: 恢复 worker-1 原有状态空 请求BtraceIdB到达 父线程 set(B) submit(task-B) → TTL 捕获快照 {traceId: B} → 包装为 TtlCallable worker-1 再次执行 → replay: 将 {traceId: B} 注入 worker-1 → worker-1 拿到 B → 执行业务逻辑能正确获取 traceIdB → restore: 恢复 worker-1 原有状态空关键点每次任务执行前都重新注入执行后都恢复原状。线程复用不再是问题因为上下文是按任务绑定的而不是按线程绑定的。对比维度InheritableThreadLocalTransmittableThreadLocal传递时机线程创建时new Thread任务执行时run/call前传递粒度按线程按任务线程池支持❌ 完全失效✅ 完美支持父子线程值隔离❌ 子线程修改不影响父线程但无法感知父线程后续修改✅ 每次任务独立捕获完全隔离实现方式JDK 原生Thread.init() 中浅拷贝装饰器模式 CRR 三步性能开销极低一次拷贝较低每次任务捕获重放恢复侵入性无侵入需包装任务或线程池Java Agent 可无侵入5、ThreadLocal如何封装使用通常会将其封装在一个工具类中如UserContextHolder并将ThreadLocal实例定义为private static final。对外提供set,get,clear方法并强制要求在 Filter、Interceptor 或 AOP 的finally块中调用clear()方法确保资源被正确清理。使用场景用户上下文传递如UserContext、RequestContextSpring 的RequestContextHolder就是基于 ThreadLocal数据库事务管理保证一个线程内使用同一个数据库连接SimpleDateFormat 线程安全包装每个线程持有一个独立的实例反模式不要用线程池中使用 ThreadLocal 而不调用 remove()必然导致内存泄漏用 ThreadLocal 做跨线程数据传递子线程拿不到父线程的 ThreadLocal 数据需要用InheritableThreadLocal存储超大对象即使调用了 remove()在高并发短生命周期场景下也可能造成瞬时内存压力推荐封装模式public class UserContextHolder { private static final ThreadLocalUserContext CONTEXT ThreadLocal.withInitial(UserContext::new); public static void set(UserContext context) { CONTEXT.set(context); } public static UserContext get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } } // 在拦截器/过滤器中统一处理 public class ContextInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { UserContextHolder.set(extractUserFromRequest(request)); return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 统一清理防止泄漏 UserContextHolder.clear(); } }