ThreadLocal 、InheritableThreadLocal 与 TransmittableThreadLocal 的进阶指南

一、从 MyBatis-Plus 的一个诡异问题说起

在一次面试中,面试官问了一个关于 MyBatis-Plus 的问题:

"在同一个事务中,用相同条件查询,为什么会读到不同的数据?"

除了常规的事务传播机制,更深层的答案是 MyBatis 的一级缓存 ​ 和 Spring 的事务同步管理器

Spring 使用 ThreadLocal 来存储 SqlSession。如果 ThreadLocal 中的数据在传递过程中"丢"了,或者使用了错误的 SqlSession,就会导致缓存失效或数据错乱。这引出了我们今天的主角:ThreadLocal 及其家族


二、ThreadLocal:线程的"私人保险柜"

1. 核心原理

ThreadLocal 的全称是 Thread Local Variable(线程局部变量) 。它并不是在线程之间共享变量,而是为每个线程提供一个变量的独立副本

误区纠正ThreadLocal 本身不存储值,它只是作为 Thread 类中 ThreadLocalMapKey

java 复制代码
public class Thread implements Runnable {
    // ThreadLocal 的值存在这里
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // 用于 InheritableThreadLocal
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

当我们调用 set(T value) 时,实际上是把值放进了当前线程(Thread.currentThread())的 threadLocals 这个 Map 里。

2. 内存结构图

下图展示了 ThreadLocal 在 JVM 内存中的存储结构(左半部分),以及三种 ThreadLocal 的能力对比(右半部分):

核心要点

  • Stack(栈) ​ 中只保存 ThreadLocal 引用变量

  • Heap(堆) ​ 中的 Thread 对象内部持有 ThreadLocalMap

  • EntryKey ​ 是 WeakReference<ThreadLocal>(弱引用)

  • EntryValue​ 是强引用

3. 内存泄漏:为什么必须 remove()

这是面试的高频考点。ThreadLocalMapEntry 对 Key 的引用是 弱引用(WeakReference) ,但对 Value 的引用是 强引用

  • Key 被回收 :当 ThreadLocal 实例被置为 null 后,GC 会将 Key(弱引用)回收,变成 null

  • Value 残留:由于 Value 是强引用,且线程(比如线程池的核心线程)一直存活,导致 Value 永远无法被回收

解决方案 :在 finally 块中调用 remove()

java 复制代码
try {
    threadLocal.set(obj);
    // ... biz logic
} finally {
    threadLocal.remove(); // 切断 Value 的强引用
}

三、InheritableThreadLocal:父子线程的"传家宝"

1. 痛点:ThreadLocal 无法在子线程获取

java 复制代码
ThreadLocal<String> tl = new ThreadLocal<>();
tl.set("父线程的值");

new Thread(() -> {
    System.out.println(tl.get()); // 输出:null
}).start();

2. 原理:创建子线程时的"拷贝"

InheritableThreadLocal (ITL) 重写了 childValuegetMapcreateMap 方法,使得在创建子线程时,JVM 会将父线程的 inheritableThreadLocals 复制给子线程。

源码逻辑(简化)

java 复制代码
// Thread 构造方法
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {
    // 关键步骤:复制父线程的 inheritableThreadLocals
    if (parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

3. 致命缺陷:线程池环境失效

ITL 的复制发生在 new Thread() 的一瞬间。但在 线程池​ 中,线程是复用的,任务提交时并没有创建新线程,因此无法获取父线程的上下文。

java 复制代码
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
ExecutorService pool = Executors.newFixedThreadPool(1);

itl.set("主线程值");
pool.execute(() -> {
    // 输出 null!因为线程池里的线程早就创建好了,不是新 new 出来的
    System.out.println(itl.get()); 
});

四、TransmittableThreadLocal:线程池的"通行证"

为了解决线程池环境下的上下文传递问题,阿里巴巴开源了 transmittable-thread-local(TTL)。

1. 核心思想:时机转移

既然线程创建时复制不行,TTL 选择在 任务被提交时(Capture) ​ 和 **任务被执行前(Replay)**​ 这两个时机做手脚。

2. TTL 执行时序图

下图完整展示了 TTL 在任务提交和执行过程中的四个阶段:

3. 工作流程详解

  1. 捕获(Capture) :当把 Runnable 提交给线程池时,捕获当前线程(父线程)的 TTL 值

  2. 包装(Wrap) :TTL 将 Runnable 包装成 TtlRunnable

  3. 回放(Replay) :线程池里的线程执行 TtlRunnable.run() 之前,将捕获的值设置到当前线程(子线程)中

  4. 恢复(Restore):任务执行完毕后,清除刚才设置的值,防止影响下一个任务

4. 代码示例

java 复制代码
// 1. 使用 TTL
TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
ttl.set("Main-Value");

ExecutorService pool = Executors.newFixedThreadPool(1);

// 2. 包装任务(关键步骤)
Runnable task = () -> System.out.println(ttl.get());
Runnable ttlTask = TtlRunnable.get(task);

// 3. 提交执行
pool.execute(ttlTask); // 输出:Main-Value

5. 应用场景

  • 全链路追踪 :SkyWalking、Zipkin 传递 TraceId

  • Session 传递:在 Web 层获取用户 Session,传递给异步线程处理

  • Spring 事务上下文:在异步方法中保持事务一致性


五、三种 ThreadLocal 对比总结

特性 ThreadLocal (TL) InheritableThreadLocal (ITL) TransmittableThreadLocal (TTL)
作用 线程内部数据隔离 父子线程间数据传递 线程池/复杂线程模型下数据传递
存储位置 threadLocals inheritableThreadLocals 包装了 ITL,并配合 TtlRunnable
复制时机 无复制 创建子线程时 ​ (new Thread()) 任务提交时 ​ (execute())
线程池支持 x 不支持 (脏数据) x 不支持 (复用导致失效) ok 完美支持
典型场景 事务上下文、SimpleDateFormat 单线程创建子线程 **微服务链路追踪 (SkyWalking)**​

六、结合 Spring 与 MyBatis-Plus 的实战思考

回到文章开头的问题,为什么 MyBatis-Plus 会读到不同数据?

  1. ThreadLocal 绑定 :Spring 使用 TransactionSynchronizationManager(内部使用 ThreadLocal)绑定了 SqlSession

  2. 一级缓存 :MyBatis 的一级缓存是 SqlSession 级别的。如果 ThreadLocal 中的 SqlSession 被意外清理(例如错误的手动管理 Session),缓存就会失效

  3. 异步陷阱 :如果在事务中使用了 @Async 或线程池,由于默认的 ThreadLocal 无法传递,子线程会开启一个全新的事务和 SqlSession,导致读到的数据与主线程不一致

最佳实践 :在涉及事务的异步操作中,如果需要上下文一致,请使用 TransmittableThreadLocal 配合特定的事务传播机制,或者避免在事务方法中开启异步线程。


七、结语

  • ThreadLocal ​ 是基础,用于线程隔离,切记用完 remove()

  • InheritableThreadLocal​ 是补充,解决了简单的父子线程传递,但在现代线程池应用中基本失效

  • TransmittableThreadLocal ​ 是终极方案,通过装饰 Runnable 实现了线程池环境下的上下文穿透,是微服务架构下的必备知识

希望这篇文章能帮你彻底理清这三者的关系。如果觉得有用,欢迎点赞、收藏、关注!


参考资料

  • 1 《Java 并发编程实战》- Brian Goetz 等

  • 2 阿里巴巴 transmittable-thread-local GitHub Wiki

  • 3 Spring Framework TransactionSynchronizationManager 源码

  • 4 MyBatis SqlSessionManager 源码分析

相关推荐
键盘会跳舞1 小时前
C++:std::pair 源码级深度剖析 —— 关联容器的基石
开发语言·c++·pair·关联容器
(Charon)1 小时前
【C++】手写线程安全队列:mutex + condition_variable 实现生产者消费者模型
c语言·开发语言·c++
北极糊的狐1 小时前
钉钉小程序报错data.formatTime is not a function是因为 axml 模板中不能直接调用 Page 内自定义方法!
java·小程序·钉钉
崖边看雾1 小时前
同步上下文管理器规则
开发语言·数据库
名字还没想好☜10 小时前
Python f-string 进阶:数字格式化、对齐填充、调试 = 号与嵌套表达式
开发语言·数据库·python·字符串格式化·f-string
·薯条大王10 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
一壶浊酒..13 小时前
Scrape Webpack练习
开发语言·javascript·webpack
ZJH__GO13 小时前
网络编程v4pro--实现聊天室文件传输功能
java·服务器·网络·计算机网络
程序员黑豆13 小时前
Windows 系统 Java 环境变量配置全攻略:解决“不是内部或外部命令”
java·前端·ai编程