假设有一个健身房,里面有很多储物柜(Locker)。每次来一个人,就分配给他(或她)一个储物柜,里面可以放这位客人的私人物品(例如手机、衣服、钥匙等)。当另一个人来健身时,也分配另一个不同的储物柜,两个客人之间不会互相影响或混用柜子。
ThreadLocal
中存储的数据对比:
把这个类比映射到程序世界,就是:
这便是 ThreadLocal
最本质的概念:给每个线程分配一个私有的存储空间,使得该线程可以在里面读写数据,而其他线程无法干涉。
在多线程环境中,如果多条线程都要访问(读写)同一个全局变量,就会遇到并发、安全、数据一致性等问题。我们可能需要加锁、加 volatile 等,或者想办法把这个变量变成方法参数层层传递,十分繁琐。
但有些场景,数据其实不需要被线程之间共享,而是“线程私有”的。举例:
如果我们希望快速地在同一个线程的上下文里保存并访问这样的数据,同时不必担心和其他线程的冲突,也避免了在方法参数间反复传递,那么 ThreadLocal
就登场了。
如果你对底层实现不感兴趣,可以跳过,但了解一下有助于理解“为什么一定要及时清理”。
ThreadLocalMap
在 Java 的实现中,每一个 Thread
(准确说是 java.lang.Thread
对象)内部,都会有一个 ThreadLocalMap
的属性。它是一个散列表结构,用来存储
这样的键值对。
当我们对某个 ThreadLocal
实例调用 set(value)
时,实际操作的是:
当前线程(Thread.currentThread()
)内部的 ThreadLocalMap
,往那张表里塞入一条记录:key = 该 ThreadLocal
对象,value = value
。
当我们对同一个 ThreadLocal
实例调用 get()
时,它会去当前线程的 ThreadLocalMap
里找 key=这个 ThreadLocal
的记录,然后把 value 取出来返回给我们。
所以,每个线程都维护着一张自己的 ThreadLocalMap
,里面可能会存多条记录。不同线程各自一张表,所以存储在其中的数据自然是互不可见的。
ThreadLocalMap
有一个特殊处理:它对 key(即 ThreadLocal
对象)使用“弱引用(WeakReference)”来避免内存泄露。但如果 ThreadLocal
对象被垃圾回收了,而我们忘记调用 remove()
去清理 Map 里的 value,那么这个 value 可能会变成”Key = null, Value = XXX“ 的悬挂条目(zombie entry),从而导致内存无法被回收,产生内存泄漏。
因此,官方建议:在使用完 ThreadLocal 后,显式调用 remove()
方法,以保证我们在后续不会出现残留数据,也更安全。
我们先写一个最简单的用法:演示如何给各个线程设置不同的值,并取出来:
public class ThreadLocalExample {
// 声明一个静态 ThreadLocal 用来存储 String
private static final ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
// 启动两个线程
Thread t1 = new Thread(() -> {
// 设置当前线程的局部变量
threadLocal.set("数据A - 来自线程T1");
// 获取并打印
System.out.println("T1得到: " + threadLocal.get());
// 用完后,清理
threadLocal.remove();
});
Thread t2 = new Thread(() -> {
threadLocal.set("数据B - 来自线程T2");
System.out.println("T2得到: " + threadLocal.get());
threadLocal.remove();
});
t1.start();
t2.start();
}
}
典型输出会是:
T1得到: 数据A - 来自线程T1
T2得到: 数据B - 来自线程T2
你会看到,“T1”和“T2”各自 set 的值不会相互影响。
一些可选用法
get()
:如果从未 set()
过,默认返回 null
。set(T value)
:把当前线程的值设为 value
,类型是你定义的泛型。remove()
:清理当前线程的这份值,非常重要。可以把 ThreadLocal
当作线程范围的存储容器。在多层调用或框架中,你不想在每个方法都增加一个形参来传递某个信息(比如 UserId),那就把它放进 ThreadLocal
,在需要的地方 get()
出来就行。
在很多后端 Web 框架(包括 Spring Boot, Tomcat 容器)中,请求进来后通常会被分配到线程池中的某个工作线程去处理。请求处理完再归还线程到池里。
在整个处理过程中(Controller -> Service -> DAO -> ...),都处于同一个线程上下文。这时,ThreadLocal
就特别有用。比如:
(1)存储“当前登录用户ID”
UserContextHolder.setUserId(userId)
(内部就是用 ThreadLocal 存储)UserContextHolder.getUserId()
即可。UserContextHolder.remove()
及时清理。(2)存储“TraceId / RequestId” 以便日志关联
ThreadLocal
原理来存储信息。(3)多租户 (Multi-Tenant) 场景
ThreadLocal
存一个 tenantId,然后在 DAO 里获取该 tenantId 并做过滤。ThreadLocal
,我们可能需要在所有的方法调用链里都加一个 Long userId
,这样非常麻烦且可读性差;ThreadLocal
,我们就“声明一次” -> “随处可取”;又不必担心多线程并发修改的问题,因为它是“当前线程私有”。remove()
, 那下一次有可能处理另一个用户的时候,再 get()
还会拿到残留的 1001,引发严重的安全漏洞或业务错误。ThreadLocal
对象本身被回收了,而你不清理它在 ThreadLocalMap
中的存储条目,就会有“key=null,但 value 还存活”的情况,造成泄漏。finally
/ afterCompletion()
阶段执行 ThreadLocal.remove()
。示例(Spring MVC 拦截器):
public class UserContextInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 解析 userId
Long userId = ... // 解析 token
UserContextHolder.setUserId(userId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 清理
UserContextHolder.removeUserId();
}
}
UserContextHolder
是一个自己封装的类:
public class UserContextHolder {
private static final ThreadLocal userIdHolder = new ThreadLocal<>();
public static void setUserId(Long userId) {
userIdHolder.set(userId);
}
public static Long getUserId() {
return userIdHolder.get();
}
public static void removeUserId() {
userIdHolder.remove();
}
}
afterCompletion()
里调用ContextHolder中定义的用于清除对应线程局部变量的ThreadLocal的remove方法。
UserContextHolder
类本身对所有线程来说是一份(因为它是静态的)。- 但
ThreadLocal
帮我们完成了数据副本的隔离,保证每个线程获取到的值都是自己那份,不会互相干扰。- 因而,“在不同线程中,
UserContextHolder
看似共享一个ThreadLocal
变量,但实际存的值各不相同”,因为它利用线程内部的ThreadLocalMap
做了隔离。
这样就保证了线程不会带着“上一次的私有数据”到下一次请求里,线程安全问题也就迎刃而解。
当有多个线程局部变量
public class UserContextHolder {
// 存储当前线程的用户ID
private static final ThreadLocal userIdHolder = new ThreadLocal<>();
// 存储当前线程的租户ID
private static final ThreadLocal tenantIdHolder = new ThreadLocal<>();
// 存储当前线程的请求追踪ID(用于日志跟踪)
private static final ThreadLocal traceIdHolder = new ThreadLocal<>();
// --------------- 用户ID ---------------
public static void setUserId(Long userId) {
userIdHolder.set(userId);
}
public static Long getUserId() {
return userIdHolder.get();
}
public static void removeUserId() {
userIdHolder.remove();
}
// --------------- 租户ID ---------------
public static void setTenantId(String tenantId) {
tenantIdHolder.set(tenantId);
}
public static String getTenantId() {
return tenantIdHolder.get();
}
public static void removeTenantId() {
tenantIdHolder.remove();
}
// --------------- 追踪ID ---------------
public static void setTraceId(String traceId) {
traceIdHolder.set(traceId);
}
public static String getTraceId() {
return traceIdHolder.get();
}
public static void removeTraceId() {
traceIdHolder.remove();
}
// --------------- 统一清理方法,防止内存泄漏 ---------------
public static void clear() {
userIdHolder.remove();
tenantIdHolder.remove();
traceIdHolder.remove();
}
}
ThreadLocal
userId
、tenantId
、traceId
。set()
/ get()
/ remove()
三个方法
setXXX()
用于存储数据getXXX()
用于读取数据removeXXX()
用于清理数据clear()
方法
ThreadLocal
变量,避免在线程池环境下的内存泄漏问题。afterCompletion()
里调用 clear()
,确保线程不会残留上次请求的数据。本质:
ThreadLocal
为每个线程都“开”了一块独立空间(可以类比“储物柜”);使用场景:
核心 API:
set(T value)
:把当前线程对应的 ThreadLocal 值设置成 value
。get()
:获取当前线程对应的值(若没设置过默认 null
)。remove()
:移除当前线程对应的值,必须养成习惯“用完就清理”。注意事项:
ThreadLocal
当作跨线程共享的工具,它只适合“线程私有”;UserContextHolder
)里完成“set/get/remove”,让代码更清晰。一句话概括
ThreadLocal
是一个“线程范围的存储工具”,能够在多线程应用中优雅地管理“仅属于当前线程”的数据,用完一定要及时清理,就能避免各种线程安全或内存泄漏问题。
ThreadLocal 与 Synchronized 的区别
ThreadLocal
用于“让每个线程各存一份数据”,从根本上避免了数据竞争;synchronized
(或锁)用于“让多个线程顺序访问共享数据,防止并发冲突”。ThreadLocal
可能更简洁。为什么要在 Web 应用尤其注意?
ThreadLocalMap 的 key 是弱引用,value 是强引用
ThreadLocal
实例本身没有被外部引用,就会被 GC 回收,而 ThreadLocalMap
里只剩下 null -> value
,value 无法释放,出现内存泄漏。MDC (Mapped Diagnostic Context) 是怎么用 ThreadLocal 的?
traceId
、userId
等;ThreadLocal
来实现的。ThreadLocal
可以认为是一种在“当前线程”里随时取用的数据存储,不会与其他线程的存储混在一起;ThreadLocal
是一个非常优雅、简洁的多线程编程“管理器”。