ThreadLocal的介绍与使用规范,初学者必看

写在最前面:文章为本人原创,且由于水平有限,请大家批判性地阅读,欢迎指正。

我写本文的主要目的还是为了教会一个不了解ThreadLocal的人(前提是学过多线程)学会它,因此文字尽量通俗易懂。

概念与作用

ThreadLocal 是 Java 提供的一种用于实现线程局部变量的类。它允许每个线程独立地存储和访问变量的副本,从而实现线程之间的数据隔离。每个线程都可以在 ThreadLocal 中存储和获取自己的值,而不会影响其他线程的值。

很多人刚学ThreadLocal时,都以为它本身是一个存值的容器,这是一个很致命的误解,如果你这么认为,那你永远也用不好它。我们平时为了说话方便,都说把xxx存在ThreadLocal中,这其实是一个逻辑存,并不是真的存在这个对象里。

一句话解释,ThreadLocal其实就是一个key,用来从某个Map中获取一个value。这个Map存在于线程对象里,所以这个Map在各个线程之间是独立的,因此通过ThreadLocal这个key获取到的value就是本线程特有的。也就是说,ThreadLocal的设计思想就是在各个线程之中创建自己的副本,来做到的线程隔离,避免了线程安全问题。

所以说,ThreadLocal适合于存放每个线程都有,但各自不同的值。比如说,可以存放用户的登录信息、日志的traceId等,特别适用于一些无法显示传参的场景等。

使用与注意点

ThreadLocal的使用非常简单,首先定义一个ThreadLocal对象,这个对象往往被定义为静态的,方便公共调用。第二步,在你获得了要设置的值之后,调用ThreadLocal对象的set方法。然后在你后续任何需要获取它的时候,都可以使用get方法取值。最后,如果可以的话一定要调用remove方法,因为线程是公用的,不remove干净,后面线程被其他请求重新调用时会有之前set的旧数据,可能造成bug。

ThreadLocal对象不需要线程隔离,所有线程都可以调用同一个ThreadLocal对象的get方法,获得的值却是不一样的。毕竟它只是一个key嘛,每个线程都可以用这个key来存储各自的value。

如何set与remove

第一个要注意的点是,ThreadLocal应该尽可能在流量的入口set,例如springboot的拦截器、过滤器、AOP中以及系统的门面方法中。因为这样,才能确保在执行完逻辑后,能够调用remove方法。例如:

typescript 复制代码
public class RequestContextInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 从请求中获取用户 ID(示例:从请求头中获取)
        String userId = request.getHeader("User-ID");
        RequestContext.getCurrentContext().setUserId(userId);
        return true; // 继续执行后续的处理
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 清理 ThreadLocal 变量
        RequestContext.clear();
    }
}

public class RequestContext {
    private static final ThreadLocal<RequestContext> contextHolder = ThreadLocal.withInitial(RequestContext::new);

    private String userId;

    public static RequestContext getCurrentContext() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

可能很多人看到这会有疑问,既然请求每次执行前都会set,那就会刷新掉旧值啊,不存在你说的读取到旧值的情况啊。我的回答是:目前你的请求入口确实set了,但随着后续迭代,如果出现了其他的入口,一个不小心就会出bug。而且,不清理的话,这个value可能不会被垃圾回收,造成内存泄漏问题。

其实由于ThreadLocal未清理造成线上事故的事情不算少,我举个例子,假如你在Service所有方法的前面都手动set,某一天系统交到别人手中了,他写一个新方法前忘记了set,但他也没get,所以没出事故。后面又有另一个人来开发系统,它一看项目里使用了ThreadLocal,认为你肯定set了,就在某个地方get了一下,是不是就出事故了。例子比较极端,但对于一个特别复杂的系统来说,如果你set的地方不统一,后面接手的人真的难以判断你到底set了没有。

多线程时该怎么办

第二个需要注意的点是,当你进行了异步操作后,就无法使用ThreadLocal了。

异步操作最多的情况就是通过线程池执行逻辑,由于ThreadLocal存放了各个线程各自的副本,因此线程池中的线程并没有调用的线程的ThreadLocal副本,所以这个时候你是get不到的,除非在创建任务时手动传参。

封装任务类

对于这种情况,可以封装一个Runnable或Callable接口的实现类,在其构造方法里重新set其ThreadLocal的值。

例如:(这段代码看不懂说明需要先学习一下线程池)

typescript 复制代码
public class ThreadLocalRunnable implements Runnable {
    private final String value1;
    private final Integer value2;

    public ThreadLocalRunnable(String value1, Integer value2) {
        this.value1 = value1;
        this.value2 = value2;
    }

    @Override
    public void run() {
        ThreadLocalContext.setThreadLocal1(value1);
        ThreadLocalContext.setThreadLocal2(value2);

        try {
            //...
        } finally {
            context.clear(); // 清理所有 ThreadLocal 变量
        }
    }
}

// 使用示例
public class Main {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        
        executorService.submit(new ThreadLocalRunnable("Value 1 for Thread 1", 10));
        executorService.submit(new ThreadLocalRunnable("Value 2 for Thread 2", 20));

        executorService.shutdown();
    }

TransmittableThreadLocal

如果这种解决方案不满足需求,可以了解一下阿里的TransmittableThreadLocal,它可以很方便地在调用线程前获取所有上下文,并在新线程中进行重放,甚至还可以在最后,调用恢复方法,将ThreadLocal的值恢复为调用前。

dart 复制代码
// ===========================================================================
// 线程 A
// ===========================================================================

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("value-set-in-parent");

// (1) 抓取当前线程的所有TTL值
final Object captured = TransmittableThreadLocal.Transmitter.capture();

// ===========================================================================
// 线程 B(异步线程)
// ===========================================================================

// (2) 在线程 B中回放在capture方法中抓取的TTL值,并返回 回放前TTL值的备份
final Object backup = TransmittableThreadLocal.Transmitter.replay(captured);
try {
    // 你的业务逻辑,这里你可以获取到外面设置的TTL值
    String value = context.get();

    System.out.println("Hello: " + value);
    ...
    String result = "World: " + value;
} finally {
    // (3) 恢复线程 B执行replay方法之前的TTL值(即备份)
    TransmittableThreadLocal.Transmitter.restore(backup);
}

相关推荐
小雅痞10 分钟前
[Java][Leetcode simple]26. 删除有序数组中的重复项
java·leetcode
青云交17 分钟前
Java 大视界 -- 基于 Java 的大数据分布式存储在工业互联网海量设备数据长期存储中的应用优化(248)
java·大数据·工业互联网·分布式存储·冷热数据管理·hbase 优化·kudu 应用
纸包鱼最好吃27 分钟前
java基础-package关键字、MVC、import关键字
java·开发语言·mvc
唐山柳林30 分钟前
城市生命线综合管控系统解决方案-守护城市生命线安全
java·安全·servlet
PgSheep33 分钟前
Spring Cloud Gateway 聚合 Swagger 文档:一站式API管理解决方案
java·开发语言
蒂法就是我1 小时前
详细说说Spring的IOC机制
java·后端·spring
程序员拂雨1 小时前
Java知识框架
java·开发语言
秋野酱2 小时前
基于javaweb的SpringBoot高校图书馆座位预约系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
举一个梨子zz2 小时前
Java—— 可变参数、集合工具类、集合嵌套、不可变集合
java·开发语言·intellij-idea·需求分析
算法给的安全感2 小时前
bfs-最小步数问题
java·算法·宽度优先