写在最前面:文章为本人原创,且由于水平有限,请大家批判性地阅读,欢迎指正。
我写本文的主要目的还是为了教会一个不了解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);
}