Appearance
JUC ThreadLocal
本文介绍ThreadLocal相关知识。
1. 什么是ThreadLocal
ThreadLocal 是 Java 提供的一个类,用于创建线程局部变量(thread-local variable)。它的核心作用是为每个使用该变量的线程都提供一个独立的、变量的副本。
什么是线程局部变量? 简单来说,当你有一个 ThreadLocal 变量时:
- 当线程 A 调用它的
get()方法时,它会得到一个值。如果是第一次调用get()方法,则会返回初始值。 - 当线程 B 调用它的
get()方法时,它会得到另一个值(可能是同一个类的实例,但不是同一个对象)。 - 线程 A 对这个值进行修改,不会影响线程 B 获取到的那个值。
- 线程 B 对它自己的值进行修改,也不会影响线程 A 获取到的那个值。
ThreadLocal 提供了一种替代方案来管理“上下文”或“会话”级别的数据,特别适用于以下场景:
避免同步的开销和复杂性: 如果某个数据在单个线程的整个执行过程中都需要用到,并且不需要被其他线程直接访问,那么与其使用共享变量加锁,不如使用
ThreadLocal为每个线程维护一个独立副本。这样每个线程访问自己的副本时,就不需要加锁,避免了同步带来的性能损耗。方便地在方法调用链中存取数据: 有些数据(如用户身份信息、事务 ID、请求上下文等)需要在同一个线程内的多个方法中访问,但又不想作为参数层层传递。可以将这些数据存储在
ThreadLocal中,方法只需要通过ThreadLocal.get()就能获取,而无需关心数据是从哪里来的。例如,在Spring Security中,用户登录信息就是存放在
ThreadLocal中的,由于每个请求进来是一个线程处理的,所以过滤器(Filter)是在同一个线程进行处理的,把登录信息存在ThreadLocal中,可以使得后续的过滤器拿到用户信息,不用作为参数传递,降低代码复杂性。
2. 用法
ThreadLocal 的核心方法如下:
set(T value): 设置当前线程中ThreadLocal变量的值为value。get(): 获取当前线程中ThreadLocal变量的值。如果当前线程是第一次调用
get(),并且之前没有调用过set(),则会调用initialValue()方法获取初始值。initialValue(): 这是一个 protected 方法,通常通过继承ThreadLocal并覆盖此方法来提供一个默认值。当线程第一次调用get()且未设置值时,就会返回initialValue()的结果。Java 8 引入了更方便的
ThreadLocal.withInitial(Supplier)方法来提供初始值。remove(): 移除当前线程中ThreadLocal变量的值。这非常重要,尤其是在线程池等复用线程的环境中,可以防止内存泄露问题。移除后,下次调用get()时会重新计算初始值。
下面以一个例子说明ThreadLocal的用法:
java
@Slf4j
public class ThreadLocalExample {
// 创建一个 ThreadLocal 变量,使用 withInitial 提供默认值
private static final ThreadLocal<String> threadLocalName = ThreadLocal.withInitial(() -> "Default Name");
public static void main(String[] args) throws InterruptedException {
// 线程 A
Thread threadA = new Thread(() -> {
log.info("Initial value: " + threadLocalName.get());
threadLocalName.set("Thread A's Name");
log.info("Set value: " + threadLocalName.get());
Sleeper.sleep(1000); // 睡眠1秒钟,期间其他线程改变ThreadLocal的值
// 再次获取,验证值是线程隔离的
log.info("Final value: " + threadLocalName.get());
// !!!!! 重要:在线程结束前移除 ThreadLocal 变量的值 !!!!!
threadLocalName.remove();
log.info("After remove: " + threadLocalName.get()); // remove 后会再次回到 initialValue
}, "Thread-A");
// 线程 B
Thread threadB = new Thread(() -> {
log.info("Initial value: " + threadLocalName.get());
threadLocalName.set("Thread B's Name");
log.info("Set value: " + threadLocalName.get());
Sleeper.sleep(2000);
}, "Thread-B");
threadA.start();
Sleeper.sleep(200); // 让 A 先运行一会儿
threadB.start();
threadA.join(); // 等待线程 A 结束
threadB.join(); // 等待线程 B 结束
log.info("value: " + threadLocalName.get()); // 主线程也有自己的值
}
}结果如下:
txt
11:03:06.420 [Thread-A] INFO : Initial value: Default Name
11:03:06.421 [Thread-A] INFO : Set value: Thread A's Name
11:03:06.621 [Thread-B] INFO : Initial value: Default Name
11:03:06.621 [Thread-B] INFO : Set value: Thread B's Name
11:03:07.422 [Thread-A] INFO : Final value: Thread A's Name
11:03:07.422 [Thread-A] INFO : After remove: Default Name
11:03:08.626 [main] INFO : value: Default Name可以看到,ThreadLocal变量在每个线程中都有一份,互不干扰。
3. 原理
3.1 threadLocals字段
首先明确一个想法:在Java中,线程是由Thread类表示的,一个线程就是一个Thread类对象。
在Thread类中有一个字段,用来存储ThreadLocal变量(这个变量是属于对象的,所以是线程私有的):
java
ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocalMap,观其名知其意,这是一个Map结构,所以存储的是key-value键值对,但是ThreadLocalMap有些许不同(ThreadLocalMap是ThreadLocal中的内部类,此处只显示ThreadLocalMap的结构):
java
static class ThreadLocalMap {
// 存储键值对的数组
private Entry[] table;
// Entry是内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}可以发现,ThreadLocalMap中的键值对,其中键是ThreadLocal变量,是一个弱引用,值就是我们实际存储的值(例如字符串、用户信息对象等等),是一个强引用。
当我们调用ThreadLocal.get()方法时:
java
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// this 是ThreadLocal对象,即通过键获取键值对
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
// 返回值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map为空,则表示该线程中的map还没有创建,此时应该创建map,返回初始值
return setInitialValue();
}setInitialValue()方法如下:
java
private T setInitialValue() {
// 获取初始值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 在map中存入键值对
map.set(this, value);
} else {
// 创建线程对应的map
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
// 创建map
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}至此,关于如何实现线程私有变量就很清晰了,就是在Thread中保存一份Map,其中存储了以ThreadLocal变量为键,以应用实际值为值的键值对。
3.2 inheritableThreadLocals
观察Thread的源码,发现不仅有threadLocals字段,还有inheritableThreadLocals字段:
java
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;这又是什么呢?
在某些场景下,父线程创建子线程时,需要将父线程的某些上下文信息传递给子线程,让子线程能够访问到这些信息。这种需求被称为上下文传播(Context Propagation)。我们就可以使用InheritableThreadLocal来实现。
当一个线程(父线程)创建另一个新线程(子线程)时,子线程会继承父线程中所有 InheritableThreadLocal 变量的当前值。 子线程会在自己的 InheritableThreadLocalMap 中拥有这些值的副本。
使用场景:
- 用户身份信息传递:在处理用户请求时,如果主线程(通常是线程池中的某个线程)验证了用户身份,将用户信息存入
ThreadLocal。如果这个请求又需要创建子线程去执行一些并行任务(如异步发送邮件、记录日志、调用其他内部服务),这些子线程可能需要知道当前是哪个用户在操作,以便进行权限检查、日志记录等。 - 请求 ID / 跟踪 ID: 在分布式系统中,一个请求会跨越多个服务和线程。通常会生成一个唯一的请求 ID 或跟踪 ID 并将其贯穿整个调用链,方便日志分析和问题追踪。将这个 ID 存在
ThreadLocal中,子线程需要继承这个 ID。
inheritableThreadLocals的工作原理:
在Thread的构造方法内,有以下代码(只展示关键代码)体现线程间的继承:
java
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// inheritThreadLocals默认为true
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}ThreadLocal.createInheritedMap代码如下:
java
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
// 将父线程中的inheriyableMap复制一份到子线程
for (Entry e : parentTable) {
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}那inheritableThreadLocals字段是在什么时候初始化的呢?InheritableThreadLocal是ThreadLocal的一个子类:
java
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
public InheritableThreadLocal() {}
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}当我们调用ThreadLocal.get()方法时,如果是第一次使用ThreadLocal变量,那么最终会调用createMap()方法,这就在线程中初始化了inheritableThreadLocals字段。
不仅仅是get()方法会创建ThreadLocalMap,set()方法也会:
java
// ThreadLocal类中的set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}下面举一个例子说明InheritableThreadLocal的使用:
java
@Slf4j
public class InheritableThreadLocalExample {
// 创建一个 InheritableThreadLocal 变量, 子线程会继承父线程设置的这个变量的值
private static final InheritableThreadLocal<NumberHolder> inheritableContext1 = new InheritableThreadLocal<>();
private static final InheritableThreadLocal<NumberHolder> inheritableContext2 = new InheritableThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
inheritableContext1.set(new NumberHolder(1));
inheritableContext2.set(new NumberHolder(1));
Sleeper.sleep(100); // 睡眠一会儿,确保子线程后执行
// 创建并启动子线程
Thread childThread = new Thread(() -> {
// 在子线程中获取 InheritableThreadLocal 的值,应该能获取到父线程在创建子线程前设置的值
NumberHolder numberHolder1 = inheritableContext1.get();
log.info("子线程第一次获取(inheritableContext1):" + numberHolder1.toString());
NumberHolder numberHolder2 = inheritableContext2.get();
log.info("子线程第一次获取(inheritableContext2):" + numberHolder2.toString());
// (可选) 在子线程中修改 InheritableThreadLocal 的值,注意,没有修改引用地址
numberHolder1.setNum(2);
log.info("修改后 inheritableContext1 值: " + inheritableContext1.get());
inheritableContext2.set(new NumberHolder(3));
log.info("修改后 inheritableContext2 值: " + inheritableContext2.get());
}, "Child-Thread");
childThread.start();
// 等待子线程执行完毕,以确保看到子线程的输出
childThread.join();
// 在父线程中再次获取值,子线程修改结果影响到了父线程
log.info("子线程结束后,inheritableContext1 值: " + inheritableContext1.get());
// 应该还是父线程设置的原始值
log.info("子线程结束后,inheritableContext2 值: " + inheritableContext2.get());
}
}
@Data
@AllArgsConstructor
class NumberHolder{
private int num;
}结果如下:
txt
12:04:00.100 [Child-Thread] INFO : 子线程第一次获取(inheritableContext1):NumberHolder(num=1)
12:04:00.101 [Child-Thread] INFO : 子线程第一次获取(inheritableContext2):NumberHolder(num=1)
12:04:00.101 [Child-Thread] INFO : 修改后 inheritableContext1 值: NumberHolder(num=2)
12:04:00.101 [Child-Thread] INFO : 修改后 inheritableContext2 值: NumberHolder(num=3)
12:04:00.101 [main] INFO : 子线程结束后,inheritableContext1 值: NumberHolder(num=2)
12:04:00.101 [main] INFO : 子线程结束后,inheritableContext2 值: NumberHolder(num=1)可以发现,父子线程中ThreadLocal引用的值,其实是指向同一块内存空间的,如果子线程修改了对象内容而不改变引用地址,那么会影响父线程中ThreadLocal引用的值;如果改变了引用地址,那么不会影响父线程。
4. 内存泄漏问题
4.1 为什么要使用弱引用作为键的类型
假设我们使用强引用作为键的类型,那么根据之前的原理分析,可以画出下面的对象关系图:

当有其他指针指向ThreadLocal对象时,此时说明我们的应用程序还需要ThreadLocal对象,它不应该被当成垃圾回收掉。
但是,如果之后我们应用程序不需要ThreadLocal对象后,对象关系图如下:

由于在应用程序中,没有指针指向ThreadLocal对象,那么该对象应该被当成垃圾回收掉。但是,从Thread这一条线看过来,会发现还有强引用指向ThreadLocal对象,此时该对象不会被当成垃圾回收掉,但是,我们程序中再也不能通过ThreadLocal变量使用ThreadLocal对象了,此时会造成内存泄漏。
因此,使用弱引用,当没有强引用指向ThreadLocal对象时,该对象会被当成垃圾回收掉:

4.2 Entry中value造成的内存泄漏问题
在Java中,键被设计为弱引用解决内存泄漏问题是一方面,但是这又带来了值造成内存泄漏的问题。
当作为key的ThreadLocal对象被当成垃圾回收后,key变为null,但是此时value还强引用地指向数据对象:

从Thread对象出发,可以到达value指向的数据对象,所以该数据对象不会被当成垃圾释放掉,但是,实际上该对象已不可达,实际上是垃圾,所以这就是另一种内存泄漏。
要解决这个问题,在线程中使用完ThreadLocal后,需要开发者手动调用remove()方法,用来移除Entry:
java
// ThreadLocal中的remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
// ThreadLocalMap中的remove()
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 通过key找到第一个索引值
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null;
// 继续找下一个索引值
e = tab[i = nextIndex(i, len)]) {
// 如果该索引值处的key等于要移除的key,那么执行移除操作
if (e.refersTo(key)) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}所以,使用ThreadLocal的“正确方式”应如下:
java
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"default");
new Thread(()->{
try {
// 使用
threadLocal.get()
}finally {
// 移除
threadLocal.remove();
}
},"t1").start();4.3 final 关键字的使用
在上面所谓的“正确方式”,其实还有一个问题,来看下面的例子:
java
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"default");
public static void main(String[] args) {
new Thread(()->{
try {
// 使用
log.info(threadLocal.get());
Sleeper.sleep(2000);
log.info(threadLocal.get());
System.gc();
// 下面是为了调试所需
Thread thread = Thread.currentThread();
log.info(thread.getName());
}finally {
// 移除
threadLocal.remove();
}
},"t1").start();
Sleeper.sleep(100);
// 改变threadLocal指向的对象
threadLocal = ThreadLocal.withInitial(()->"!!!!!!!");
}运行上面的程序,结果如下:
java
14:52:53.136 [t1] INFO : default
14:52:55.142 [t1] INFO : !!!!!!!
14:52:55.152 [t1] INFO : t1这是正确的,但是上面的程序隐含着内存泄漏问题,我们在第16行代码打上断点,检查t1线程的threadLocals字段:

我们分析上面的代码流程:
- 首先
ThreadLocal变量threadLocal指向第一个对象(我们称为A); - 然后启动t1线程,使用threadLocal变量,此时在其threadLocalMap中添加一条键值对(A-default字符串);
- t1线程休眠2秒,模拟执行其他任务;
- 主线程在t1线程执行后开始执行,将
ThreadLocal变量threadLocal指向第二个对象(我们称为B); - t1线程结束休眠后,再次使用threadLocal变量,此时由于threadLocal所指向的对象已经变了 ,所以会在t1线程的threadLocalMap中再添加一条键值对(B-!!!!!!!字符串);
- t1线程执行垃圾回收,由于第一个
ThreadLocal对象A没有强引用指向它,所以被回收掉,第一条键值对中的键(referent字段)变为null;如果线程t1长时间存活,那么第一条键值对中的值(default字符串)会造成内存泄漏。 - 即使t1线程最后使用完
ThreadLocal调用了remove()方法,也只是释放第二条键值对,第一条键值对仍然保留。
出现上面的问题,就是由于ThreadLocal变量threadLocal在线程使用过程中发生了改变,所以我们应该限制ThreadLocal变量threadLocal不允许改变,加final关键字。
最终,总结出使用ThreadLocal的正确用法:
java
// final关键字保持threadLocal指向的ThreadLocal对象不变
final ThreadLocal<String> threadLocal = ThreadLocal.withInitial(()->"default");
new Thread(()->{
try {
// 使用
threadLocal.get()
}finally {
// 移除
threadLocal.remove();
}
},"t1").start();