InheritableThreadLocal在@Async中失效,因其仅在线程创建时复制父线程值,而线程池复用线程导致后续任务继承脏状态;Shiro靠显式remove()兜底,可靠方案是手动透传或使用TransmittableThreadLocal。

InheritableThreadLocal 为什么在 @Async 中失效
因为 @Async 底层用的是线程池,而 InheritableThreadLocal 只在 新线程创建时 复制父线程值,不处理线程复用场景。
线程池里的线程是预先创建、反复使用的。第一次任务执行时,线程从主线程继承了 InheritableThreadLocal 值;但第二次任务进来,同一线程可能已执行过其他请求,其内部的 inheritableThreadLocals 映射表早已被上一轮任务清空或覆盖 —— 此时它“继承”的是自己上次运行后的脏状态,不是当前请求的上下文。
- 主线程设置
TenantContextHolder.set("t1")后触发@Async - 线程池分配一个空闲线程(比如
thread-5),该线程首次启动,会复制主线程的InheritableThreadLocal值 → 正确拿到"t1" - 同一
thread-5接着执行另一个请求的任务,此时它携带的是上一次遗留的"t2"或 null → 数据写入错误租户
对比 new Thread() 和线程池中 InheritableThreadLocal 的行为差异
new Thread() 每次都新建 JVM 级线程,JVM 在 init() 阶段强制复制父线程的 inheritableThreadLocals,所以父子值一定一致;而线程池绕过了这个机制。
Spring 的 ThreadPoolTaskExecutor 默认使用 java.util.concurrent.ThreadPoolExecutor,它调用 newWorkerThread() 时传入的是已有线程对象(非新构造),InheritableThreadLocal 的复制逻辑根本不会触发。
-
new Thread(() -> { ... }).start():每次都是全新线程 →InheritableThreadLocal有效 -
executor.submit(() -> { ... }):线程来自池 →InheritableThreadLocal仅首次有效,后续不可靠 -
@Async本质就是封装了submit()调用 → 同样不可靠
为什么 Shiro 能用 InheritableThreadLocal 却不出问题?
Shiro 的 SecurityManager 初始化时,会把 Subject 绑定到 InheritableThreadLocal,但它在每次请求结束前显式调用 remove(),确保线程归还线程池前清空上下文。这不是 InheritableThreadLocal 本身可靠,而是靠人工兜底。
而多数业务自定义的 TenantContextHolder 或 UserContext 往往只做 set(),忽略 remove(),导致脏数据残留。
- 漏掉
remove()是最常见误操作,尤其在异常分支中 - 即使加了
remove(),也无法解决“线程刚被分配就执行任务”时继承到旧值的问题 - Shiro 的“可用”是建立在严格生命周期管理 + 不依赖异步线程池的前提下
真正能落地的替代方案只有两个
别再试图修 InheritableThreadLocal,它不是为线程池设计的。要么放弃隐式传递,要么换能穿透线程池的工具。
- 手动透传:在
@Async方法签名里加参数,把TenantContextHolder.get()结果作为入参传进去,方法体内再set()回去 → 清晰、可控、无副作用 - 用
TransmittableThreadLocal(TTL):它通过装饰Runnable和Executor,在任务提交和执行前后自动快照/恢复上下文 → 需要改造所有线程池入口,包括@Async的TaskExecutorBean
线程池上下文传递不是“能不能”的问题,是“谁负责清理、谁负责快照、谁保证不泄漏”的责任划分问题。一旦选错路径,QPS 上去后丢租户标识就是必然结果,不是偶发。










