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);
}

相关推荐
小小小妮子~19 分钟前
设计模式七大设计原则Java 实践
java·设计模式
快乐非自愿6 小时前
一文解秘Rust如何与Java互操作
java·开发语言·rust
小万编程6 小时前
基于SpringBoot+Vue毕业设计选题管理系统(高质量源码,提供文档,免费部署到本地)
java·vue.js·spring boot·计算机毕业设计·java毕业设计·web毕业设计
m0_748235076 小时前
使用rustDesk搭建私有远程桌面
java
快乐是6 小时前
发票打印更方便
java
文浩(楠搏万)6 小时前
Java内存管理:不可达对象分析与内存泄漏优化技巧 Eclipse Memory Analyzer
java·开发语言·缓存·eclipse·内存泄漏·不可达对象·对象分析
圆蛤镇程序猿6 小时前
【什么是MVCC?】
java·数据库·oracle
m0_748256786 小时前
【SQL】掌握SQL查询技巧:数据分组与排序
java·jvm·sql
Damon撇嘴笑6 小时前
Cause: java.sql.SQLException: sql injection violation, comment not allow异常问题处理
java·数据库·sql
孟秋与你6 小时前
【redisson】redisson分布式锁原理分析
java·分布式