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为空时自动初始化
相关推荐
liangshanbo121516 分钟前
虚拟列表深度面试题整理
java·开发语言·前端
破土士V21 分钟前
MySQL基础知识集合
数据库·mysql
gugucoding28 分钟前
59. 【Java】Spring Boot 入门:第一个 Web 应用
java·开发语言·spring boot
笃行35041 分钟前
达梦 VS 金仓:数据库替换项目里,迁移工具的差距体现在哪一层
数据库
笃行35043 分钟前
向量数据库之后,一份合同为什么在五个库里对不上
数据库
IT_陈寒1 小时前
Redis内存警告竟是因为这个不起眼的配置项
前端·人工智能·后端
笃行3501 小时前
数据库迁移工具装在一台笔记本上的年代,该翻篇了
数据库
笃行3501 小时前
一张表底下几千个分片:金仓时序数据库的超表怎么工作
数据库
Python私教1 小时前
AI 漫剧角色一进分镜就变脸?把提示词升级成“角色 ID + 镜头合同”
后端
Python私教1 小时前
一次生成 20 个角色却全都撞脸:我用“角色合同”重做了批量生成流程
后端