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为空时自动初始化
相关推荐
梨涡泥窝8 小时前
基于SSM的校园二手交易平台的设计与实现
java·tomcat
Winner_hwx8 小时前
华为ICT大赛备赛复习1
服务器·网络·华为
许彰午9 小时前
44-useRowSet镜像实现
java·低代码·架构
AugustRed9 小时前
Neo4j 图数据库原理 + 应用场景简单介绍
数据库·neo4j
宗介波妞9 小时前
Wireshark 抓取 IEC104 报文:常见异常报文排查与标志位解读
服务器·网络·wireshark
Gl�ria9 小时前
Redis 高可用架构对比
数据库·redis
bamboolm9 小时前
springboot+Ollama整合
java·ollama
IpdataCloud10 小时前
高可用的IP数据接口平台有哪些?从可用性、P99延迟到离线部署的评估框架(含代码)
数据库·tcp/ip·ip
我不会起名字32210 小时前
一天一道算法题(34):回溯法的经典例题(子集)
java·数据结构·python·算法·golang·深度优先·力扣
呆萌很10 小时前
MySQL 数据库和表的管理操作(命令行)
数据库·mysql