前面讲了 读写锁 因为 AQS 只能记录写线程,无奈之下,只好把读线程的信息记录到各自线程的 ThreadLocal 中,那 ThreadLocal 中,那 ThreadLocal 里的数据就没有并发问题吗?
并发问题是多线程对共享变量同时进行修改,如果每个线程都有一个"独占的储物柜",即拥有自己的独立变量,那么就不存在多个线程竞争修改同一个变量的问题。本篇介绍的 ThreadLocal 就是这个"独占的储物柜"。
下面看一下 ThreadLocal 基本使用,再分析一下实现原理。
基本使用
ThreadLocal 的核心方法主要用于为每个线程提供独立的变量副本,并控制这些副本的创建、访问和删除。
| 方法 | 作用 | 说明 |
|---|---|---|
set(T value) |
将值存入当前线程的私有副本 | 这个值对于当前线程来说是独立的,其他线程无法访问或修改。 |
get() |
获取当前线程之前存入的值 | 如果当前线程是第一次调用 get(),并且没有通过 set() 设置过值,则会通过调用 initialValue() 返回默认值 |
remove() |
移除当前线程的值 | 这个方法在多线程环境中可以防止内存泄漏,特别是在线程池中使用时尤为重要 |
initialValue() / withInitial() |
设置默认值 | 通常在第一次调用 get() 且没有 set() 时会调用该方法。默认实现返回 null,但是可以通过子类覆盖此方法或者通过 ThreadLocal.withInitial() 提供一个初始化函数 |
ThreadLocal 用法还是挺简单的,无非增(set)删(remove)改(set)查(get)而已,比如每个线程都有自己的计数器:
java
package com.cango.thread.threadlocal;
public class ThreadLocalExample {
// 创建一个 ThreadLocal 变量,每个线程都有自己的初始值 0
private static ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 定义一个任务,增加计数器并打印当前线程的计数值
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
// 获取当前线程的计数器值
Integer counter = threadLocalCounter.get();
// 增加计数器
threadLocalCounter.set(counter + 1);
// 打印当前线程的计数值
System.out.println(Thread.currentThread().getName() + " 的计数器值: " + threadLocalCounter.get());
}
};
// 启动两个线程,执行相同的任务
Thread thread1 = new Thread(task, "线程1");
Thread thread2 = new Thread(task, "线程2");
thread1.start();
thread2.start();
}
}
输出结果如下:
js
线程1 的计数器值: 1
线程2 的计数器值: 1
线程2 的计数器值: 2
线程1 的计数器值: 2
线程2 的计数器值: 3
线程1 的计数器值: 3
线程2 的计数器值: 4
线程1 的计数器值: 4
线程2 的计数器值: 5
线程1 的计数器值: 5
可以看到,两个线程操作同一个 ThreadLocal 对象,但数据互不干扰,各自独立。
如果将 threadLocalCounter 换成普通的 int 变量,每个线程对 threadLocalCounter 进行自增,肯定会有并发问题。
那为啥每个线程从相同的 ThreadLocal 对象中获取的变量都是线程独立的呢?
如何做到在多个线程对同一个共享变量进行 set 操作时,实现线程的完全隔离呢?
自定义 ThreadLocal
如果让我实现 ThreadLocal 的功能,我肯定会想到使用 Map,将线程作为 key,那每个线程都可以对应不同的 value,互不打扰,美滋滋。
java
public class MyThreadLocal<T> {
// 使用静态的 ConcurrentHashMap 来存储每个线程的变量值
private static ConcurrentHashMap<Thread, Object> threadLocalMap = new ConcurrentHashMap<>();
// 获取当前线程的变量值,如果没有则返回初始值
public static <T> T get() {
Thread currentThread = Thread.currentThread();
return (T) threadLocalMap.computeIfAbsent(currentThread,null);
}
// 为当前线程设置变量值
public static <T> void set(T value) {
Thread currentThread = Thread.currentThread();
threadLocalMap.put(currentThread, value);
}
// 移除当前线程的变量值
public static void remove() {
Thread currentThread = Thread.currentThread();
threadLocalMap.remove(currentThread);
}
}
如上,threadLocalMap 使用 Thread 作为 key,存储每个线程的独立变量值。
ConcurrentHashMap是专门为多线程高并发场景而设计的Map。
这样做简单是简单,但有个问题就是这个 threadLocalMap 是静态的,生命周期与 JVM 一致。
当线程销毁后,Thread 对象作为 Key 虽然不再被使用,但仍然被 Map 强引用着,导致已销毁的 Thread 对象无法被 GC 回收,Thread 对象内部持有的所有资源(包括它的栈、本地变量等)都无法释放,线程创建得越多,泄漏越严重,这不完犊子了。
下面就看看 Josh Bloch 和 Doug Lea 这两个大神是怎么利用弱引用实现的这个线程唯一的存储柜的。
Josh Bloch 曾担任首席 Java 架构师,主导了许多 Java 标准库的设计和实现,著有《Effective Java》一书,被视为学习 Java 语言的经典入门书籍。
Doug Lea 是并发编程领域的专家,最大贡献之一就是开发了
java.util.concurrent包,他是《Concurrent Programming in Java》一书的作者,这本书也是并发编程的经典著作之一。
源码分析
先看一下ThreadLocal 类结构上的嵌套关系:

ThreadLocalMap 是 ThreadLocal 的静态内部类 ,Entry 又是 ThreadLocalMap 的静态内部类。
再来一张 UML 类图。

⨳ Thread :作为数据的主人,内部有 threadLocals 字段,指向该线程专属的 ThreadLocalMap。
java
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
因为是线程私有,所以不同线程操作同一个 ThreadLocal 时,实际读写的是各自不同的 Map,天然隔离,不需要加锁。
⨳ ThreadLocalMap :存储容器,底层是一个 Entry[] 数组。
js
static class ThreadLocalMap {
private static final int INITIAL_CAPACITY = 16; // 初始容量为16
private Entry[] table; // 底层数组结构
private int size = 0; // 当前存储的元素数量
数组的初始容量为 16,当元素太多时,会扩展数组。
⨳ Entry:实际数据单元。
java
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 存储当前 ThreadLocal 的值
Entry(ThreadLocal<?> k, Object v) {
super(k); // 将 ThreadLocal 的弱引用作为 key
value = v; // 将线程的局部变量作为 value
}
}
Entry 的 key 是 ThreadLocal 实例的 弱引用 ,value 是 ThreadLocal 对应的值。
⨳ ThreadLocal:访问入口,提供 set/get/remove,本身不存数据。
感觉是不是有点复杂了,下面看一下 set 方法,把这几个类串起来。
java
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果当前线程的 ThreadLocalMap 不为 null,则在 map 中存储值
if (map != null) {
map.set(this, value);
} else {
// 如果当前线程还没有 ThreadLocalMap,创建一个新的 map
createMap(t, value);
}
}
ThreadLocal 实例本身是全局共享的,通常声明为 static final,那就是说调用 ThreadLocal 的 set 方法时,是多个线程调用同一个对象的 set 方法。
所以 set 方法第一步,就是获取调用该方法该线程的专属 Map,然后把自己(this)作为 key 存入该线程的专属 Map。
最终的结果就是,同一个 ThreadLocal 对象,在不同线程的专属 Map 中,是同一个 key,但对应不同的 value 。
一个Thread 多个 ThreadLocal
既然是 Map,那就意味着 同一个线程 可以用多个 ThreadLocal 作为自己的存储柜。

总得来说,Thread 是储物柜的主人,ThreadLocalMap 是它专属的柜子,Entry 是柜子里的格子,而 ThreadLocal 只是开格子用的钥匙。
java
public class MultiThreadLocalExample {
private static final ThreadLocal<String> userSession = ThreadLocal.withInitial(() -> null);
private static final ThreadLocal<Integer> userId = ThreadLocal.withInitial(() -> 0);
public static void setUserSession(String session) {
userSession.set(session);
}
public static String getUserSession() {
return userSession.get();
}
public static void setUserId(Integer id) {
userId.set(id);
}
public static Integer getUserId() {
return userId.get();
}
public static void main(String[] args) {
Runnable task = () -> {
String threadName = Thread.currentThread().getName();
setUserSession(threadName + "'s Session");
setUserId(threadName.length());
System.out.println(threadName + " - Session: " + getUserSession());
System.out.println(threadName + " - User ID: " + getUserId());
};
new Thread(task).start();
new Thread(task).start();
}
}
在这个例子中,每个线程都有自己的 userSession 和 userId。输出结果如下:
js
Thread-1 - Session: Thread-1's Session
Thread-1 - User ID: 8
Thread-0 - Session: Thread-0's Session
Thread-0 - User ID: 8
也可以将 ThreadLocal 实例当做储物柜的票据,根据这个小票就可以获得对应的 value,如果小票不同,取不到值不能怪储物柜不好使。所以一般会将 ThreadLocal 实例声明成静态常量。
弱引用与内存泄漏
那为啥搞这么复杂,既然对于多线程来说,Thread 是多个,Thread 持有的 ThreadLocalMap 也是相互不同的实例,ThreadLocalMap 本就相互隔离,还把 ThreadLocalMap 设计这么复杂干啥?
直接让 Thread 持有 一个普通的 HashMap ,省去 ThreadLocal 这个操作Map 的类,用户直接操作 Thread 对象持有的 HashMap 可以吗?
可以是可以,但不安全,也没啥规范。
⨳ 规范问题
如果用字符串 或者 普通对象做 key:
js
// 模块A
Thread.currentThread().threadLocalMap.put("user", userDto);
// 模块B,第三方包,也写了
Thread.currentThread().threadLocalMap.put("user", traceId);
两个完全无关的模块,不小心用了同一个字符串 key,互相覆盖数据 ,bug 极其隐蔽,多线程环境很难复现;而且多人协作、多模块、多框架混在一起的时候,字符串 key 就是定时炸弹,哪怕大家命名很规范也防不住冲突,你用 "user" 存用户信息,我还用 "USER" 存呢,他还用 "usr" 存呢?
而 ThreadLocal 实例本身就是唯一 key , 每个业务定义自己的 static final ThreadLocal<User> USER_TL,每个 ThreadLocal 对象是独一无二的。 别的模块就算变量名也叫 USER_TL,也是不同对象,不会发生 key 碰撞。
ThreadLocalMap 不实现 Map 接口,对外完全不可见,只能通过 ThreadLocal 的 get/set 间接操作,这就大大避免了大家对 key 的约定有冲突的问题。
⨳ 内存泄漏
而且 HashMap 全部是强引用,在线程池场景下,线程不会销毁,业务代码临时创建一个 Object 作为 key 存入 map,业务结束后,没有任何地方引用这个 key 对象了。 但是 HashMap 强引用保存 key,key 对象永远无法 GC,value 也无法 GC,这就导致了内存泄漏。
ThreadLocal 怎么解决这个问题的呢?
ThreadLocalMap.Entry 的 Key 是用弱引用包装过的:
java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
前文讲过JVM的四种引用,强引用 只有对象存活(GC Roots 可达),GC 永远不会回收 ,软引用 当 GC 后内存仍然不足时回收;弱引用 ,是只要对象只剩弱引用可达,GC 发生时就会被回收。
当外部没有强引用持有 ThreadLocal 实例时,key 会被 GC ,钥匙可以被回收,但格子里的东西不会自动清空,所以当后续调用 get()/set()/remove() 及扩容时会触发启发式清理,尝试清除 key 为 null 的过期 Entry。
当然,弱引用仅仅是兜底方案,针对ThreadLocal 实例本身生命周期很短 的场景,我们业务里绝大多数都是 static final ThreadLocal,所以还得养成用完 remove() 的习惯,可以将其看作是一种资源,放到 finally 里面释放。
应用场景
ThreadLocal 用于保存线程私有变量,多线程之间互相隔离,变量绑定线程,不是绑定任务,如果数据属于"当前线程/当前请求"的,才适合放 ThreadLocal;需要跨线程共享的,就不适合了。
⨳ 链路透传上下文
Web / RPC 服务,一次请求贯穿很多层:Controller → Service → Mapper。 需要传递:userId、traceId、请求 IP。如果所有方法签名都要加 traceId 参数,代码污染严重。
那就可以在拦截器 / 过滤器入口 set,业务代码任意地方 get,方法不用改参数;请求结束 finally remove。
js
// 全局静态TL
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
// 过滤器
try {
TRACE_ID.set(generateTraceId());
doFilter();
} finally {
TRACE_ID.remove();
}
比如还可以在一次请求内埋点收集临时指标,贯穿整个调用链路,请求结束丢弃。
⨳ 线程内复用资源
同一个线程多次调用方法,可以复用同一个资源实例。
比如旧版 Spring 事务,把数据库连接放到 ThreadLocal,保证同一个线程内所有数据库操作拿到同一个 Connection,从而实现事务统一提交 / 回滚。
比如 SimpleDateFormat 不是线程安全的,不能用 static 修饰全局共享,这种就可以每个线程放一份这个资源。
js
private static final ThreadLocal<SimpleDateFormat> SDF_TL
= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
当然现在推荐线程安全的 DateTimeFormatter ,不再推荐使用 SimpleDateFormat 了。
多个线程同时对同一个对象的同一个属性进行并发修改时,才可能会产生并发问题。DateTimeFormatter 是个不可变对象
Immutable Object,实例一旦创建成功,内部所有状态永远不会被修改,成员变量都用 final 修饰,不会被修改那肯定就没有并发问题喽。
javapublic final class DateTimeFormatter { private final CompositePrinterParser printerParser; private final Locale locale; private final DecimalStyle decimalStyle; private final ResolverStyle resolverStyle; private final Set\<TemporalField\> resolverFields; private final Chronology chrono; private final ZoneId zone;
总结
ThreadLocal 其实是线程特有存储模式 (Thread Specific Storage,TSS)在 Java 里的经典实现。在 G 语言中,这种实现叫 pthread_key_t,在 Python 中叫 threading.local()...
线程特有存储模式:在多线程应用程序中,不同线程可能需要独立地访问和维护各自的数据,TSS 模式允许每个线程拥有自己的数据副本,而不会相互干扰。
Specific 是 "特有的" 的意思,Storage 是 "存储" 的意思,每个线程特有的存储,意味着存在里面的数据对于每个线程都是独立的,不会引起并发问题。
附:UML 类图
java
@startuml
class Thread {
+ ThreadLocal.ThreadLocalMap threadLocals
}
class "ThreadLocal.ThreadLocalMap" as ThreadLocalMap {
- Entry[] table
}
class "ThreadLocalMap.Entry" as Entry {
+ ThreadLocal<?> key // 弱引用
+ Object value // 强引用
}
class ThreadLocal {
+ set(T value)
+ get()
+ remove()
}
Thread --> ThreadLocalMap : has >
ThreadLocalMap o-- Entry : contains >
ThreadLocal ..> ThreadLocalMap : uses as Key >
@enduml