ThreadLocal详解

一、是什么?作用

ThreadLocal 是线程本地存储工具 ,可以给当前线程单独绑定私有变量副本,多个线程之间完全隔离、互不干扰。

  • 每个线程独有一份数据,其他线程读不到;
  • 不用加锁就能实现线程安全,规避并发争抢;
  • 典型用途:在线程生命周期内,传递上下文信息(登录用户、traceId、数据源标记)。

直观例子

java 复制代码
ThreadLocal<String> tl = new ThreadLocal<>();

// 线程1设置
new Thread(() -> {
    tl.set("用户A");
    System.out.println(tl.get()); // 用户A
}).start();

// 线程2设置,互不影响
new Thread(() -> {
    tl.set("用户B");
    System.out.println(tl.get()); // 用户B
}).start();

同一个ThreadLocal对象,不同线程set/get互不干扰,无并发竞争。

二、底层存储结构

1. 存储位置

数据不存放在 ThreadLocal 对象里 ,而是存在 Thread 对象内部的 ThreadLocalMap:

  1. 每个Thread实例自带:ThreadLocal.ThreadLocalMap threadLocals
  2. ThreadLocalMap 内部是 Entry[] 数组
    • Entry 键:弱引用 ThreadLocal<?> 对象
    • Entry 值:真正存储的业务数据
  3. 调用 tl.set(value):
    获取当前线程 Thread → 拿到它的 ThreadLocalMap → 以当前tl为key存入value

结构图简化:

复制代码
Thread-1
 └─ threadLocals(ThreadLocalMap)
     ├─ Entry(key=tl1, value=用户信息)
     └─ Entry(key=tl2, value=traceId)

Thread-2
 └─ threadLocals(ThreadLocalMap)
     ├─ Entry(key=tl1, value=另一个用户)

2. get/set/remove 源码极简流程

  1. set(T value)
    拿到当前线程Thread → 获取map,map不存在则新建;map以当前ThreadLocal为key存入value
  2. get()
    当前线程获取map,根据tl对象key查找Entry,返回value;无数据返回null
  3. remove()
    当前线程map中删除该tl对应的Entry,主动释放内存(必做)

三、典型实战场景

场景1:全局传递登录用户上下文(Spring拦截器)

不用每层接口、service、mapper层层传参,线程内任意位置获取当前登录人。

java 复制代码
// 1. 全局上下文工具类
public class UserContextHolder {
    private static final ThreadLocal<LoginUser> USER_TL = new ThreadLocal<>();

    // 设置
    public static void setUser(LoginUser user) {
        USER_TL.set(user);
    }
    // 获取
    public static LoginUser getUser() {
        return USER_TL.get();
    }
    // 移除!关键
    public static void clear() {
        USER_TL.remove();
    }
}

拦截器解析token,存入上下文:

java 复制代码
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    String token = request.getHeader("token");
    LoginUser user = parseToken(token);
    UserContextHolder.setUser(user);
    return true;
}

// 接口、Service任意处直接获取,不用传参
@GetMapping("/info")
public Result info() {
    LoginUser user = UserContextHolder.getUser();
    return Result.success(user);
}

// 后置处理器必须清理
@Override
public void afterCompletion(...) {
    UserContextHolder.clear();
}

场景2:线程内链路追踪 TraceId 透传

MDC底层就是封装了ThreadLocal,打印日志自动带上链路ID,不用每层传递。

场景3:多数据源动态切换标记

同一线程内标记要使用的数据源,DAO层直接读取,不用参数传递。

场景4:SimpleDateFormat 线程不安全解决方案

SimpleDateFormat 非线程安全,不能static共用;

给每个线程单独实例一份,用ThreadLocal隔离:

java 复制代码
private static final ThreadLocal<SimpleDateFormat> DATE_TL = ThreadLocal.withInitial(
        () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);

四、关键问题:内存泄漏(高频考点)

1. 为什么会内存泄漏?

  1. ThreadLocalMap.Entry 的key是弱引用(WeakReference),JVM GC时,ThreadLocal对象会被回收,key变成null;
  2. 但Entry的value是强引用,不会自动释放;
  3. 线程池复用线程(核心线程常驻),线程不会销毁,Entry(key=null, value=xxx)永远残留在map里,value无法被GC,内存泄漏堆积。

2. 泄漏触发完整链路

  1. 业务方法结束,ThreadLocal引用置空;
  2. GC回收ThreadLocal,Entry.key = null;
  3. 线程池核心线程长期存活,Thread对象不销毁,map不会销毁;
  4. value强引用常驻内存,无法释放 → 内存泄漏。

3. 怎么彻底避免?

使用完毕必须手动调用 tl.remove()

  • 普通单次线程:线程结束自动销毁Thread,map跟着销毁,泄漏概率低;
  • 线程池环境(Tomcat线程池、自定义业务线程池):100%必须remove,必写finally清理。

标准安全模板:

java 复制代码
try {
    UserContextHolder.setUser(user);
    // 执行业务
} finally {
    UserContextHolder.clear(); // remove 强制清理
}

key设计成弱引用,是为了尽可能减少泄漏,只是不能彻底根除,必须配合手动remove。

五、ThreadLocal 父子线程传递问题

1. 原生ThreadLocal:子线程拿不到父线程数据

主线程set值,新开子线程new Thread(),子线程get()返回null;

因为父子线程是两个独立Thread对象,各自的ThreadLocalMap完全隔离。

2. 解决方案:InheritableThreadLocal

继承类,会在创建子线程时,自动把父线程map里的键值拷贝一份到子线程。

java 复制代码
private static final InheritableThreadLocal<String> TL = new InheritableThreadLocal<>();

局限:只在新建线程时拷贝一次 ,线程池复用线程时失效,无法多次透传;

线程池异步场景要用阿里 TransmittableThreadLocal(TTL) 解决。

六、常用API汇总

方法 作用
set(T value) 当前线程存入数据
get() 当前线程读取数据
remove() 删除当前线程本条键值对,释放内存
withInitial(Supplier) 设置初始值,get为空时自动初始化
相关推荐
Wang's Blog9 小时前
Java 项目实战: 交友项目 - 圈子功能实现:总结
java·开发语言·交友
SL_staff10 小时前
JVS私有化文档系统上线避坑指南:5大运维与业务协同断点及检测加固实践
java·开源·全栈
数字新视界11 小时前
一体化机柜在数据中心运维中的创新应用与优势分析
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
IT_陈寒11 小时前
Java静态变量初始化坑得我头皮发麻
前端·人工智能·后端
FinelyYang11 小时前
考试系统简答题 AI 阅卷:覆盖度驱动、异步评分与人机协同落地实践
java·大数据·人工智能
hhhjhl11 小时前
Yup 跨端三方库仓颉适配全流程
java·开发语言
SelectDB11 小时前
从 StarRocks 走向 Apache Doris:如何实现引擎平滑替换与架构升级?
大数据·数据库·数据分析
步行cgn11 小时前
Thymeleaf 编辑表单详解:从字段回填到模拟 PUT 请求
后端
为思念酝酿的痛11 小时前
MySQL的操作
数据库·mysql
运维行者_11 小时前
网络监控工具怎么选?从设备监控到智能运维的能力清单
运维·服务器·开发语言·网络·人工智能·支持向量机·php