三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

ThreadLocal介绍

ThreadLocal介绍

目录

ThreadLocal

一、场景引入

二、核心底层原理

基础结构说明

set () /get () 完整执行流程

线程隔离本质

三、代码示例

四、重要注意事项

五、生活化类比


ThreadLocal

一、场景引入

Tomcat 依靠线程池处理客户端请求,每一个用户请求都会分配线程池中的一条工作线程执行,请求处理完成后线程回收复用。由于每条请求由独立线程执行,我们可以借助 ThreadLocal 实现线程数据隔离,让每条线程只操作属于自身的数据,常用来存储登录用户信息、请求上下文、链路追踪 ID 等数据。

二、核心底层原理

基础结构说明

每个 Thread 线程对象内部自带专属容器 ThreadLocalMap,该容器归当前线程独有;多个不同的 ThreadLocal 实例,可以共用当前线程这一张 ThreadLocalMap 完成数据存取。区分:ThreadLocal 只是提供存取数据的操作工具,并不是存储数据的容器,真正存储数据的载体是线程内部的 ThreadLocalMap。

set () /get () 完整执行流程

set (value):获取当前执行线程 → 获取该线程独有的 ThreadLocalMap → 以当前 ThreadLocal 实例作为 key,存入 value。get ():获取当前执行线程 → 获取线程的 ThreadLocalMap → 使用 ThreadLocal 实例作为 key 查询对应数据。

线程隔离本质

多个线程共用同一个全局 ThreadLocal 静态对象操作数据时,数据分别保存在各自线程内部的 ThreadLocalMap 中,天然互不干扰,以此实现线程隔离。

📌 常见误区澄清(重点补充)静态的 ThreadLocal 实例是全局唯一,所有线程共享同一个 ThreadLocal 对象。很多人会产生疑问:多个线程共用同一个 key,CPU 时间片切换交替执行,会不会出现数据相互覆盖?原理解答:传统认知中「同一个 Map 内 key 重复会覆盖 value」这条规则依旧成立,但是每条线程拥有独立的 ThreadLocalMap。共用的 ThreadLocal 只是 key 对象,但是存取操作发生在不同线程相互隔离的 Map 容器中,跨 Map 不存在 key 冲突、数据覆盖问题。通俗理解:相当于同一把钥匙,去不同房间各自独立的储物柜存放物品,互不干扰。补充设计思考:不推荐每个线程单独 new ThreadLocal,如果每个线程创建独立 ThreadLocal 实例(不同的 key),就失去了统一的数据访问入口,违背 ThreadLocal 设计目标。

三、代码示例

public class ThreadLocalDemo { // 定义全局静态ThreadLocal对象,多个线程共用这同一个对象 private static final ThreadLocal<String> contextTl = new ThreadLocal<>(); public static void main(String[] args) { // 线程1 new Thread(() -> { contextTl.set("用户A的上下文信息"); System.out.println(Thread.currentThread().getName() + ":" + contextTl.get()); // 使用完毕清除数据 contextTl.remove(); }, "线程1").start(); // 线程2 new Thread(() -> { contextTl.set("用户B的上下文信息"); System.out.println(Thread.currentThread().getName() + ":" + contextTl.get()); contextTl.remove(); }, "线程2").start(); } }

运行结果:

线程1:用户A的上下文信息 线程2:用户B的上下文信息
说明:两个线程使用同一个 contextTl 对象调用 set、get,读取到各自独立的数据,互不影响。即便操作系统切换线程时间片交替执行,也不会发生数据覆盖。

四、重要注意事项

注意 1:在线程池场景(Tomcat 工作线程池、自定义线程池),线程会被重复复用。任务 / 请求处理完成后,必须调用 threadLocal.remove () 清理数据,避免下一个复用该线程的任务读取遗留脏数据。
⚠️区分:并发执行多条独立线程不会互相覆盖数据;脏数据问题来源于线程复用,不是并发调度冲突。
注意 2:ThreadLocalMap 的 key 设计为弱引用,目的尽可能缓解内存泄漏风险;但 value 依旧存在强引用关系,弱引用无法完全杜绝内存泄漏,不能替代手动执行 remove 操作。
注意 3:同一个线程中,可以声明多个 ThreadLocal 实例,所有数据都会存放在该线程唯一的 ThreadLocalMap 中,依靠不同 ThreadLocal 实例作为 key 区分数据。
注意 4:隐藏陷阱:如果向 ThreadLocal 存放可变共享对象。虽然 ThreadLocal 隔离性生效,各线程 Map 保存独立引用;但是多个引用指向堆中同一个对象,任意线程修改对象内部属性,其他线程会受到影响。这不属于 ThreadLocal 失效,是共享可变对象引发的并发问题。
注意5:是否需要为每个线程新建独立 ThreadLocal,以使用key的弱引用回收?不需要,也不推荐。ThreadLocal 的设计目标就是全局共用同一个实例作为访问入口,依靠每条线程独立的 ThreadLocalMap 实现隔离。
两种使用形式对比:
  1. static final 全局 ThreadLocal(项目标准用法,存储请求上下文、登录信息) 静态变量长期持有强引用,Entry 内弱引用 key 无法被 GC 回收,JVM 自动清理机制失效;任务结束必须手动 remove (),不能指望 GC 自动释放。
  2. 方法内局部 ThreadLocal(极少业务场景)方法运行结束后强引用消失,GC 可以回收 ThreadLocal 对象,触发 Map 自动清理过期 Entry;但清理时机完全不可控,依然不建议依赖该机制。
总结:弱引用只是兜底防护手段,不能替代 remove () 操作。无论哪种写法,在线程池场景,请求处理完成都要手动清理。

五、生活化类比

每条线程 = 一间独立办公室,自带储物柜(ThreadLocalMap)ThreadLocal = 一把专属钥匙set:拿着钥匙,在当前办公室储物柜存放物品get:拿着钥匙,在当前办公室储物柜取出物品同一把钥匙去到不同办公室,操作的都是每个办公室自己的储物柜,互不影响。
← 返回列表