【面试真题拆解】你知道ThreadLocal是什么吗

ThreadLocal 是线程专属的本地变量,它的设计是给每个线程创建一份独立的变量副本,线程之间完全不共享数据,天然实现线程隔离,全程无锁竞争,性能远高于加锁保证线程安全的方案。

每个 Thread 内部都有一个专属的成员变量 ThreadLocalMap(不是 ThreadLocal 的!),Key 是 ThreadLocal 实例本身(弱引用),Value 是存的变量(强引用)。

官话太多,直接上例子。

以用户上下文传递举个例子

java 复制代码
/**
 * 用户上下文工具类
 * 用ThreadLocal存当前登录用户的ID,避免层层传参
 */
public class UserContextHolder {
    // 定义ThreadLocal,存用户ID
    private static final ThreadLocal<Long> USER_ID_THREAD_LOCAL = new ThreadLocal<>();

    /**
     * 设置用户ID
     */
    public static void setUserId(Long userId) {
        USER_ID_THREAD_LOCAL.set(userId);
    }

    /**
     * 获取用户ID
     */
    public static Long getUserId() {
        return USER_ID_THREAD_LOCAL.get();
    }

    /**
     * 必须手动调用!清理ThreadLocal,防止内存泄漏
     */
    public static void clear() {
        USER_ID_THREAD_LOCAL.remove();
    }
}

和加锁保证线程安全的区别

对比项 加锁 ThreadLocal
思想 多线程排队用同一个变量,保证同一时间只有一个线程能访问 每个线程用自己的专属变量,完全不共享
性能 有锁竞争,高并发下性能差 无锁竞争,性能高
适用场景 多线程必须共享同一个变量的场景(比如共享计数器、共享资源修改) 每个线程需要独立变量副本的场景(比如用户上下文、SimpleDateFormat 线程安全)

小贴士

很多人以为ThreadLocalMap的Key是"线程ID",其实Key是ThreadLocal实例本身,而不是线程ID。

一个线程可以有多个ThreadLocal实例,每个实例对应一个Value,存到同一个ThreadLocalMap里,Key不同,互不干扰。

ThreadLocal内存泄漏问题

Java中的4种引用强度

  1. 强引用

我们平时写的 Object obj = new Object() 就是强引用,只要强引用还在,GC就不会回收这个对象。

  1. 软引用

用 SoftReference 包装的引用,只有当内存不够用的时候,GC 才会回收这个对象。

  1. 弱引用

用 WeakReference 包装的引用,只要GC一运行,不管内存够不够,都会回收这个对象。

  1. 虚引用

用 PhantomReference 包装的引用,完全不影响对象的生命周期,只是用来在对象被 GC 回收前收到一个通知,做一些收尾工作。

ThreadLocalMap的Entry结构

ThreadLocalMap的Entry是继承自WeakReference的,结构如下:

java 复制代码
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k); // Key是弱引用
        value = v; // Value是强引用
    }
}

假设创建了一个 ThreadLocal 实例,有外部强引用指向它,同时把它作为 Key 存到了线程的 ThreadLocalMap 里;

当用完 ThreadLocal 后,没有手动 remove,而且外部的强引用也没了(比如 threadLocal = null),因为 Key 是弱引用,GC 一运行,Key 就被回收了,变成了 null。

但是, Value 是强引用,只要线程还活着,Value 就不会被 GC 回收,这就导致了内存泄漏。

更严重的还有,如果用了线程池,线程会被长期复用,不仅内存泄漏,还会导致用户信息串号、数据错误等严重的业务问题。

怎么避免内存泄漏

使用完ThreadLocal后,手动调用 remove() 方法。

举个例子,在Spring Boot的拦截器里:

java 复制代码
@Component
public class UserInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 从Token里解析出用户ID,存到ThreadLocal
        Long userId = parseUserIdFromToken(request.getHeader("Authorization"));
        UserContextHolder.setUserId(userId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        // 在这里清理,不管请求成功失败,都要清理。
        UserContextHolder.clear();
    }
}
相关推荐
AI你一生一世1 小时前
不止于时序存储:DolphinDB 如何成为 AI 时代的工业智能底座
java·人工智能·struts·时序数据库·工业智能·dolphindb·ai基础设施
云-缩影1 小时前
拆解 AI 图像生成应用的后端架构:ECS、OSS 和大模型 API 怎么配合
java·spring boot·后端
瑞克-桑切斯2 小时前
7、MySQL 慢查询排查与优化实战指南
java·服务器·数据库
海的透彻6 小时前
dockerfile如何快速构建容器(java服务)
java·开发语言
code2cat6 小时前
Java工具篇之Gradle:用任务图组织构建与交付
java·开发语言·单元测试·gradle
后台模板学习7 小时前
大模型 Prompt 工程在电商订单系统中的踩坑记录
java·python·prompt
w_zero_one7 小时前
算法--构造
java·数据结构·算法
SL_staff7 小时前
制造业订单交付崩盘?用四层协同框架重建交付可信度(技术实践)
java·产品·全栈
IT规划师7 小时前
编程术语中的同步与异步:从"等不等"到工程取舍
后端·面试
SL_staff8 小时前
打破风控迭代死循环:用决策编排重构业务与技术协作链路
java·产品·全栈